Golang中的循环依赖关系和接口

时间:2013-12-04 16:12:56

标签: go

我是一名长期以来的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(),可以打开数据库会话。因此,应用程序包中的应用程序调用{​​{1}}并打开数据库会话。

database.connect()的情况也是如此,它具有允许应用程序将其子路由添加到主路由对象的功能。

设置包更多地是关于函数而不是数据/常量。这包含应用程序包中的应用程序使用的代码,否则必须在所有应用程序中复制这些代码。因此,如果我需要更改路由器类,我只需要更改settings/routes.py,应用程序将继续工作而不进行任何修改。

3 个答案:

答案 0 :(得分:62)

这里有两个高级部分:确定哪个代码包含在哪个包中,并调整API以减少对包依赖的需求。

设计避免某些导入的API:

  • 编写配置函数,用于在运行时将程序包连接到彼此,而不是编译时间。它不是routes导入定义路由的所有包,而是 export routes.Registermain(或每个应用程序中的代码)可以调用。通常,配置信息可能流经main或专用包;将它分散太多会使其难以管理。

  • 传递基本类型和interface值。如果您依赖于某个类型名称的包,也许您可​​以避免这种情况。也许处理[]Page的某些代码可以改为使用[]string个文件名或[]int个ID或更通用的接口(sql.Rows)。

  • 考虑使用'架构'只包含纯数据类型和接口的包所以User与可能从数据库加载用户的代码分开。它不必依赖很多(可能是任何东西),所以你可以从任何地方包含它。 Ben Johnson gave a lightning talk at GopherCon 2016建议并按依赖关系组织包。

将代码组织到包中:

  • 作为一项规则,当每个部分在自己的上有用时,会拆分一个包。如果两个功能真的密切相关,那么你根本不必将它们分成包;您可以组织多个文件或类型。大包装可以;例如,Go {'{1}}就是一个。

  • 按主题或依赖关系分解抓包包(net/httputils否则您最终可能会导入巨大的tools包(并承担所有依赖关系)的一个或两个功能(如果分离出来,就不会有这么多依赖)。

  • 考虑推送可重复使用的代码' down'从您的特定用例中解决的低级别包。如果您的utils包含内容管理系统的逻辑和通用的HTML操作代码,请考虑移动HTML内容&#34 ;向下"到package page,这样您就可以使用它而无需导入不相关的内容管理内容。


在这里,我重新安排了一些内容,因此路由器不需要包含路由:相反,每个应用包都会调用package html方法。这就是the Gorilla web toolkit's mux package的作用。您的router.Register()routesdatabase软件包听起来像应该由您的应用代码导入的低级部分,而不是导入它。

通常,尝试在图层中构建您的应用。您的更高层,特定于用例的应用程序代码应该导入更低层,更基础的工具,而不是相反。以下是一些想法:

  • 软件包适用于从调用方的角度分离可独立使用的功能位。对于内部代码组织,您可以轻松地在程序包中的源文件之间重排代码。您在constantsx/foo.go中定义的符号的初始命名空间只是包x/bar.go,并且根据需要分割/连接文件并不困难,尤其是在帮助下像x这样的实用程序。

    标准库的goimports约为7k行(计数注释/空白但不测试)。在内部,它分为许多较小的文件和类型。但我认为这是一个软件包,因为没有理由用户会想要自己进行cookie处理。另一方面,net/httpnet 分开的,因为它们在HTTP之外使用。

    如果你可以推送" down"那就太棒了实用程序进入独立的库并感觉就像他们自己的抛光产品,或者干净地将应用程序本身分层(例如,UI位于API顶部,位于某些核心库和数据模型之上)。同样地,"水平"分离可以帮助您掌握应用程序(例如,UI层分为用户帐户管理,应用程序核心和管理工具,或者比这更细粒度的东西)。但是,核心观点是,你可以随意分裂或不作为你的作品

  • 设置API以在运行时配置行为,这样您就不必在编译时导入它。因此,例如,您的URL路由器可以公开{ {1}}方法,而不是导入net/urlRegister等,并从每个方法中读取appA。您可以制作导出appB的{​​{1}}个包以及所有观看和调用var Routes。基本思想是路由器是无需导入应用程序视图的通用代码。

    汇总配置API的一些方法:

    • 通过myapp/routesrouter s传递应用行为: router.Register可以传递interface的自定义实现(当然)还funchttpHandlerCookieJar可以接受可以从模板访问的功能(在File中)。

    • 根据需要从您的软件包中导出快捷方式功能:text/template中,来电者可以制作和单独配置一些html/template个对象,或者拨打{{1} }使用全局FuncMap。这为您提供了一个很好的设计 - 一个对象中的所有东西,调用者可以在一个进程中创建多个http,但 提供了一种懒惰的配置方式在简单的单服务器情况下。

    • 如果必须,只需用胶带粘贴它:如果你不适合自己,那么你不必限制自己超级优雅的配置系统你的应用:也许对某些内容http.Server使用全局http.ListenAndServe(...)非常有用。 但要注意全球conf的缺点。如果您想编写可重用的库,他们无法导入Server;他们需要接受他们在构造函数中所需的所有信息等.Globalbals还冒着硬连接的风险,假设在最终赢得的时候,应用程序范围内总会有一个单一的值。也许今天你有一个数据库配置或HTTP服务器配置等,但总有一天你不会。

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

  • 将基本任务与应用程序相关的任务分开。我用另一种语言处理的一个应用程序有一个" utils"模块混合一般任务(例如,格式化日期时间或使用HTML)与特定于应用程序的东西(取决于用户模式等)。但是用户包导入了utils,创建了一个循环。如果我移植到Go,我将移动依赖于用户的工具" up"在utils模块之外,可能会使用用户代码甚至高于它。

  • 考虑拆分抓包。稍微扩大最后一点:如果两个功能是独立的(也就是说,如果你将一些代码转移到另一个包,事情仍然有效与用户的观点无关的,他们将候选人分成两个包。有时捆绑是无害的,但有时它会导致额外的依赖,或者不太通用的包名称只会产生更清晰的代码。因此,我的上述Server可能会被主题或依赖关系分解(例如,package "myapp/conf"var Conf map[string]interface{}等。如果您以这种方式结束了很多套餐,我们已经goimports帮助管理它们。

  • 使用基本类型和myapp/conf替换API中的导入要求对象类型。假设您应用中的两个实体具有多对多关系,例如{{1 s和utils s。如果他们住在不同的套餐中(一个大的'如果'),则strutil返回dbutilinterface返回User因为这需要包相互导入。

    但是,如果没有Group,您可以更改其中一个或两个,例如u.Groups()个ID或[]group.Group或其他g.Users()特定的对象类型。根据您的使用情况,[]user.User[]uint等类型可能如此紧密相关,只是将它们放在一个包中会更好,但如果您认为它们应该是不同的,那么这是一种方式。

感谢您提供详细的问题和跟进。

答案 1 :(得分:0)

可能的部分但丑陋的答案: 与进口循环依赖问题斗争了一年。有一段时间,能够解耦足够多,因此没有导入周期。我的应用程序大量使用插件。同时,它使用编码/解码库(json 和 gob)。对于这些,我有自定义的 marshall 和 unmarshall 方法,以及 json 的等效方法。
为了使这些工作,包括包名称在内的完整类型名称在传递给编解码器的数据结构上必须相同。编解码器的创建必须在一个包中。这个包是从其他包和插件中调用的。 只要编解码器包不需要调用任何调用它的包,或者使用方法或方法的接口,一切都可以工作。为了能够在插件中使用包中的类型,插件必须与包一起编译。由于我不想在插件的构建中包含主程序,这会破坏插件的重点,因此插件和主程序中只包含编解码器包。在主程序调用编解码器包之后,一切正常,直到我需要从编解码器包调用到主程序。这将导致导入周期。为了摆脱这种情况,我可以将编解码器放在主程序中而不是它自己的包中。但是,因为编组/解组方法中使用的特定数据类型在主程序和插件中必须相同,所以我需要使用每个插件的主程序包进行编译。此外,因为我需要主程序调用插件,所以我需要主程序中插件的接口类型。从来没有找到让这个工作的方法,我确实想到了一个可能的解决方案: 首先,将编解码器分成一个插件,而不仅仅是一个包 然后,将其作为主程序的第一个插件加载。 创建注册函数以与底层方法交换接口。 所有的编码器和解码器都是通过调用这个插件来创建的。 插件通过注册的接口回调主程序。 主程序和所有插件为此使用相同的接口类型包。 但是,实际编码数据的数据类型在主程序中引用 使用不同的名称,但与插件中的基础类型相同,否则存在相同的导入周期。做这部分需要做一个不安全的演员。写了 一个强制转换的小函数,以便语法清晰: (Cast().

编解码器的唯一另一个问题是确保当数据发送到编码器时,它被强制转换,以便编组/解组方法识别数据类型名称。为了更简单,可以从一个包中导入主要程序类型,也可以从另一个包中导入插件类型,因为它们不相互引用。

非常复杂的解决方法,但不知道还有什么方法可以使这项工作发挥作用。 还没试过这个。一切都完成后,可能仍会以导入周期告终。

[更多内容]

为了避免导入循环问题,我使用了一种使用指针的不安全类型方法。首先,这是一个带有小函数 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{}”作为空指针并根据需要进行适当的转换。

这仅在 interface{} 指向的底层类型在任何地方都相同时才有效。为了使这更容易,我将类型放在一个单独的文件中。在调用包中,它们以包名开头。然后我复制类型文件,将包更改为“package main”,并将其放在插件目录中,以便构建类型,而不是包名称。

可能有一种方法可以对实际数据值执行此操作,而不仅仅是指针,但我还没有让它正常工作。

我做过的一件事是转换为接口而不是数据类型指针。这允许您使用插件方法将接口发送到包,其中存在导入周期。接口有一个指向数据类型的指针,然后您可以使用它从调用插件的包中调用来自调用者的数据类型上的方法。

这样做的原因是数据类型在插件之外不可见。也就是说,如果我加载到插件中,它们都是包main,并且类型都在包main中定义,但是是同名的不同类型,则类型不冲突。

但是,如果我将一个公共包放入两个插件中,则该包必须相同,并且具有编译来源的确切完整路径名。为了适应这一点,我使用 docker 容器来进行构建,以便我可以强制路径名对于我的插件中的任何常见容器始终正确。

我确实说过这很难看,但确实有效。如果因为一个包中的类型使用另一个包中的类型然后尝试使用第一个包中的类型而导致导入循环,则方法是做一个插件,使用 interface{} 擦除这两种类型。然后,您可以根据需要在接收端来回调用方法和函数进行转换。

总结: 使用 interface{} 制作空指针(即无类型)。 使用 Cast() 将它们强制为与底层指针匹配的指针类型。使用插件类型本地化,以便在单独的插件和主程序中的 main 包中的类型不冲突如果您在插件之间使用公共包,则所有构建的插件和主程序的路径必须相同。使用插件包加载插件,交换函数指针

对于我的一个问题,我实际上是从主程序中的一个包调用一个插件,只是为了能够回调到主程序中的另一个包,避免两个包之间的导入循环。我使用带有自定义编组器方法的 json 和 gob 包遇到了这个问题。我在主程序和其他插件中使用自定义编组的类型,同时,我希望插件独立于主程序构建。我通过使用包含在主程序和插件中的 json 和 gob 编码/解码自定义方法包来实现这一点。但是,我需要能够从编码器方法回调到主程序,这给了我导入循环类型冲突。上述解决方案与另一个插件专门解决导入周期的工作原理。它确实创建了一个额外的函数调用,但我还没有看到任何其他解决方案。

希望这有助于解决这个问题。

答案 2 :(得分:-8)

基本上你的代码是高度耦合的,Golang强制你保持软件包低度耦合,但是在一个软件包中,高内聚是可以的。

与python相比,Golang在包管理方面要优越得多。 在python中,您甚至可以动态导入包。

对于大项目,golang将确保您的包更易于维护。