Go语言 循环依赖关系和接口

yh2wf1be  于 2022-12-07  发布在  Go
关注(0)|答案(3)|浏览(154)

我是一个很长时间的python开发者。我正在尝试Go语言,把一个现有的python应用程序转换成Go语言。它是模块化的,对我来说非常好用。
在Go语言中创建相同的结构时,我似乎遇到了循环导入错误,比我想的要多得多。在python中从来没有遇到过任何导入问题。我甚至从来没有使用过导入别名。所以我可能遇到了一些在python中不明显的循环导入。我真的觉得很奇怪。
不管怎样,我在Go语言中试图修复这些问题时迷路了。我读到过接口可以用来避免循环依赖。但是我不明白如何做到这一点。我也没有找到任何关于这方面的例子。有人能帮助我吗?
当前的Python应用程序结构如下:

/main.py

/settings/routes.py      contains main routes depends on app1/routes.py, app2/routes.py etc
/settings/database.py    function like connect() which opens db session
/settings/constants.py   general constants

/apps/app1/views.py      url handler functions
/apps/app1/models.py     app specific database functions depends on settings/database.py
/apps/app1/routes.py     app specific routes

/apps/app2/views.py      url handler functions
/apps/app2/models.py     app specific database functions depends on settings/database.py
/apps/app2/routes.py     app specific routes

settings/database.py具有connect()这样的通用函数,它打开一个数据库会话。因此,apps包中的一个应用程序调用database.connect()并打开一个数据库会话。
settings/routes.py的情况也是如此,它具有允许应用将其子路线添加到主路线对象的功能。
设置包更多的是关于函数而不是数据/常量。它包含了apps包中的应用程序使用的代码,否则这些代码必须在所有的应用程序中复制。所以,如果我需要改变路由器类,例如,我只需要改变settings/router.py,应用程序将继续工作,而不需要修改。

njthzxwz

njthzxwz1#

这里有两个高层次的部分:弄清楚哪些代码放在哪个包中,调整API以减少对包的依赖。
关于设计避免某些导入的API:

  • 编写config函数,* 在运行时而不是编译时 * 将包挂在一起 *。routes不需要导入所有定义路由的包,它可以 * 导出 * routes.Registermain(或每个应用程序中的代码)可以调用routes.Register。一般来说,配置信息可能通过main或一个专用包流动;过于分散会使其难以管理。
    • 传递基本类型和interface值。* 如果你依赖于一个包来获得类型名,也许你可以避免这样做。也许一些处理[]Page的代码可以使用[]string的文件名或[]int的ID或一些更通用的接口(sql.Rows)来代替。
    • 考虑使用只包含纯数据类型和接口的“schema”包,* 这样User就与可能从数据库加载用户的代码分开了。它不需要依赖太多(可能依赖任何东西),所以你可以从任何地方包含它。BenJson在GopherCon 2016上做了一个 lightning 般的演讲,建议这样做,并根据依赖关系来组织包。

在将代码组织到包中时:

  • 作为一个规则,* 当每一部分都可以单独使用的时候,将一个包拆分 *。如果两个功能确实密切相关,您根本不需要将它们拆分成包;你可以用多个文件或类型来组织,大的包也可以;比如,Go语言的net/http就是1。
    • 按主题或依赖项分解grab-bag包(utilstools)。* 否则,您可能最终会为一个或两个功能块(如果分离出来,就不会有这么多依赖项)导入一个巨大的utils包(并承担其所有依赖项)。
    • 考虑将可重用的代码“向下”推到与您的特定用例分离的较低级别的包中。* 如果您有一个package page,其中包含用于您的内容管理系统的逻辑和通用的HTML操作代码,请考虑将HTML内容“向下”移动到package html中,这样您就可以使用它,而无需导入不相关的内容管理内容。

在这里,我会重新安排一些东西,这样路由器就不需要包含路由:routesdatabaseconstants软件包听起来像是低级的片段,应该由应用代码导入,而不是导入它。
一般来说,尝试分层构建应用。你的高层、特定用例的应用代码应该导入底层、更基础的工具,而不是相反。下面是一些想法:

    • 包 * 对于从 * 调用者 * 的Angular 分离独立可用的功能位是很好的。对于您的 * 内部 * 代码组织,您可以轻松地在包中的源文件之间移动代码。您在x/foo.gox/bar.go中定义的符号的初始命名空间只是包x,并且根据需要拆分/连接文件并不困难。特别是在goimports等实用程序的帮助下。

标准库的net/http大约有7 k行(包括注解/空白,但不包括测试)。在内部,它被分成许多更小的文件和类型。但我认为它是一个包,因为用户没有理由只需要它自己处理cookie。另一方面,netnet/url * 是 * 分开的,因为它们在HTTP之外有用途。
如果您 * 能够 * 将实用程序“下推“到独立的库中,感觉就像它们自己的抛光产品,或者干净地将应用程序本身分层,那就太好了(例如,UI位于API之上,而API位于一些核心库和数据模型之上)。同样,“水平”分离可以帮助您将应用程序保留在脑海中(例如,UI层分为用户帐户管理、应用程序核心和管理工具,或者比这些更细粒度的东西)。但是,核心点是,* 你可以自由地分割,也可以不分割 *。

***设置API以在运行时配置行为,这样就不必在编译时导入它。**例如,URL路由器可以公开Register方法,而不是导入appAappB你可以创建一个myapp/routes包来导入router和你所有的视图并调用router.Register。基本思想是路由器是通用代码,不需要导入应用程序的视图。

将配置API组合在一起的一些方法:

    • 通过interface s或func s传递应用行为:* http可以传递Handler的自定义实现(当然),也可以传递CookieJarFiletext/templatehtml/template可以接受从模板(在FuncMap中)访问的函数。
    • 如果合适,从包中导出快捷函数:* 在http中,调用者可以创建并单独配置一些http.Server对象,或者调用使用全局Serverhttp.ListenAndServe(...)。这给了您一个很好的设计--所有东西都在一个对象中,调用者可以在一个进程中创建多个Server等等--但是它 * 也 * 提供了一种在简单的单服务器情况下进行配置的懒惰方式。
    • 如果你不得不这样做,就用胶带把它粘起来:* 如果你不能为你的应用安装一个超级优雅的配置系统,你不必把自己限制在这个系统上:也许对于某些东西来说,带有全局var Conf map[string]interface{}package "myapp/conf"是有用的。但是要注意全局conf的缺点。如果你想编写可重用的库,它们不能导入myapp/conf;他们需要接受构造函数中所需的所有信息。全局变量还冒着硬连接的风险,假设某些东西在应用程序范围内总是有一个单一的值,但最终不会;也许今天您只有一个数据库配置或HTTP服务器配置等,但有一天您就没有了。

移动代码或更改定义以减少依赖性问题的一些更具体的方法:

***将基础任务与应用程序相关任务分开。**我用另一种语言开发的一个应用程序有一个混合了一般任务的“utils”模块(例如,格式化日期时间或使用HTML)与应用程序特定的内容(这取决于用户模式等)。但是用户包导入了实用程序,形成了一个循环。如果我移植到Go语言,我会将依赖于用户的utils从utils模块中“上移”出来,也许是为了与用户代码一起使用,甚至是在用户代码之上。
***考虑拆分垃圾袋 Package 。*稍微放大最后一点:如果两个功能是独立的(也就是说,如果你把一些代码移到另一个包中,事情仍然可以工作) 和 * 从用户的Angular 来看是不相关的,它们是被分成两个包的候选者。或者一个不太通用的包名只会使代码更清晰。(例如,strutildbutil等)。如果您以这种方式处理了大量软件包,我们可以使用goimports来帮助管理它们。
***将API中需要导入的对象类型替换为基本类型和interface。**假设应用中的两个实体具有多对多关系,如UserGroup。如果它们位于不同的包中(一个很大的“if”),您不能同时让u.Groups()返回[]group.Groupg.Users()返回[]user.User,因为这需要包相互导入。

但是,您可以更改其中的一个或两个返回值,例如,一个[]uint的ID或一个sql.Rows或其他interface,您可以在不使用import的情况下返回特定的对象类型。根据您的用例,UserGroup这样的类型可能关系密切,最好将它们放在一个包中,但是如果你决定它们应该是不同的,这是一种方法。
感谢您的详细问题和跟进。

zbwhf8kr

zbwhf8kr2#

可能偏了,但回答难看:一年来,我一直在为导入循环依赖问题而挣扎。有一段时间,我能够充分解耦,这样就不会有导入循环。我的应用程序大量使用插件。同时,它使用编码/解码库(json和gob)。对于这些,我有自定义的马歇尔和unmarshall方法,以及json的等效方法。
为了使这些工作,包括包名在内的完整类型名必须与传递给编解码器的数据结构相同。编解码器必须在一个包中创建。这个包既可以从其他包也可以从插件中调用。只要编解码器包不需要调用任何调用它的包,一切都可以正常工作。或者使用方法或方法的接口。为了能够在插件中使用包中的类型,插件必须与包一起编译。因为我不想在插件的构建中包含主程序,这会破坏插件的要点,插件和主程序中只包含了编解码器包。在主程序调用编解码器包之后,我需要从编解码器包调用主程序之前,一切都正常。这将导致导入循环。为了消除这一点,我可以将编解码器放在主程序中,而不是它自己的包中。但是,因为在主程序和插件中,封送/拆送方法中使用的特定数据类型必须相同,所以我需要使用每个插件的主程序包进行编译。此外,因为我需要主程序来调用插件,所以我需要主程序中插件的接口类型。由于一直没有找到一种方法来实现这一点,我想到了一个可能的解决方案:首先,将编解码器分离到一个插件中,而不仅仅是一个包中。然后,从主程序中加载它作为第一个插件。创建一个注册函数来与底层方法交换接口。所有的编码器和解码器都是通过调用这个插件来创建的。插件通过注册的接口回调到主程序。主程序和所有的插件为此使用相同的接口类型包。然而,实际编码数据的数据类型在主程序中被引用,其名称与插件中的不同,但底层类型与插件中的相同,否则存在相同的导入循环。要完成这一部分,需要执行不安全的强制转换。编写了一个小函数,执行强制转换,以便语法清晰:(〈cast pointer type*〉Cast(〈指向结构的指针或指向结构的指针的接口〉)。
编解码器的另一个问题是确保当数据被发送到编码器时,它被强制转换,以便马歇尔/unmarshall方法识别数据类型名称。为了更容易,可以从一个包导入主程序类型,从另一个包导入插件类型,因为它们不相互引用。
非常复杂的解决方法,但不知道如何使此工作。还没有尝试过此方法。当一切都完成时,可能仍会以导入循环结束。
[more在此]
为了避免导入循环问题,我使用了一种使用指针的不安全类型方法。首先,这里有一个包,它带有一个小函数Cast()来执行不安全类型转换,以使代码更容易阅读:

package ForcedCast

import (
    "unsafe"
    "reflect"
)

// cast function to do casts with to hide the ugly syntax
// used as the following:
// <var> = (cast type)(cast(input var))
func Cast(i interface{})(unsafe.Pointer) {
    return (unsafe.Pointer(reflect.ValueOf(i).Pointer()))
}

Next I use the "interface{}" as the equivalent of a void pointer:

package firstpackage
type realstruct struct {
     ...
}   

var Data realstruct

// setup a function to call in to a loaded plugin
var calledfuncptr func(interface)

func callingfunc() {

        pluginpath := path.Join(<pathname>, "calledfuncplugin")
        plug, err := plugin.Open(pluginpath)

        rFunc, err := plug.Lookup("calledfunc")
        calledfuncptr = rFunc.(interface{})

        calledfuncptr (&Data)
}

//in a plugin
//plugins don't use packages for the main code, are build with -buildmode=plugin
package main

// identical definition of structure
type realstruct struct {
     ...
}   

var localdataptr *realstruct

func calledfunc(needcast interface{}) {

    localdataptr = (*realstruct)(Cast(needcast))

}

对于任何其他包的跨类型依赖关系,请使用“interface{}”作为void指针,并根据需要进行适当的强制转换。
只有当interface{}指向的底层类型在任何地方都是相同的时,这才有效。为了简化这一点,我把类型放在了一个单独的文件中。在调用包中,它们以包名开始。然后我复制了一个类型文件,把包改为“package main”,并把它放在插件目录中,这样类型就被构建了,但包名没有。
可能有一种方法可以对实际的数据值(而不仅仅是指针)执行此操作,但我还没有使其正确工作。
我所做的一件事是转换为接口而不是数据类型指针。这允许你使用插件方法将接口发送到包,其中有一个导入循环。接口有一个指向数据类型的指针,然后你可以用它从调用插件的包的调用者调用数据类型上的方法。
这样做的原因是数据类型在插件之外是不可见的。也就是说,如果我加载到插件,这两个插件都是包main,并且类型在包main中定义,但是是具有相同名称的不同类型,类型不会冲突。
然而,如果我在两个插件中放置了一个公共包,这个包必须是相同的,并且具有完全相同的编译路径名。为了适应这一点,我使用了一个docker容器来进行构建,这样我就可以强制路径名对于我的插件中的任何公共容器总是正确的。

我确实说过这很难看,但它确实有效。如果因为一个包中的类型使用了另一个包中的类型,而另一个包又试图使用第一个包中的类型而导致导入循环,那么方法就是做一个插件,用interface{}擦除这两个类型。然后,您可以根据需要在接收端来回调用方法和函数来进行强制转换。
总而言之:使用接口{}生成void指针(也就是不具型别)。请使用Cast使用插件类型本地化使包main中的类型在单独的插件中,和在主程序中不冲突如果你在插件之间使用一个公共包,所有构建的插件和主程序的路径必须相同。使用插件包加载插件,并交换函数指针
对于我的一个问题,我实际上是从主程序中的一个包向外调用一个插件,只是为了能够回调到主程序中的另一个包,避免两个包之间的导入循环。我在使用json和gob包时遇到了这个问题,并使用了自定义的封送器方法。我在主程序和其他插件中都使用了自定义封送的类型。同时,我希望插件的构建独立于主程序。我通过使用包含在主程序和插件中的json和gob编码/解码自定义方法包来实现这一点。然而,我需要能够从编码器方法回调到主程序,这给了我导入周期类型冲突。上面的解决方案与另一个插件专门解决导入周期的工作。它确实创建了一个额外的函数调用,但我还没有看到任何其他的解决方案。
希望这对这个问题有帮助。

nkkqxpd9

nkkqxpd93#

下面是一个简短的问题答案(使用接口),它不会影响其他答案的正确性和完整性:
UserService导致了循环导入,它不应该真正从AuthorizationService中调用,它只是用来提取用户细节,所以我们可以在一个单独的接收端接口UserProvider中只声明所需的功能:
https://github.com/tzvatot/cyclic-import-solving-exaple/commit/bc60d7cfcbd4c3b6540bdb4117ab95c3f2987389
基本上,提取一个只包含接收方所需功能的接口,并使用它,而不是声明对外部事物的依赖。

相关问题