Golang中的循环依赖和接口

我是一个很长的Python开发人员。 我正在尝试Go,将现有的Python应用程序转换为Go。 它是模块化的,对我来说真的很好。

在Go中创build相同的结构后,我似乎陷入循环导入错误,比我想要的要多得多。 在Python中从来没有任何import问题。 我甚至从来没有使用导入别名。 所以我可能有一些循环import,这在python中是不明显的。 我其实觉得很奇怪

无论如何,我迷路了,试图在Go中解决这些问题。 我已经读过,接口可以用来避免循环依赖。 但我不明白如何。 我也没有find任何这方面的例子。 有人可以帮我吗?

目前的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()这样的通用函数,可以打开数据库会话。 因此,应用程序包中的一个应用程序调用database.connect()并打开一个db会话。

settings/routes.py的情况也是如此,它具有允许应用程序将其子路线添加到主路线对象的function。

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

这里有两个高层次的部分:找出哪些代码出现在哪个包中,调整API以减less包需要处理的依赖性。

在devise避免需要某些导入的API时:

  • 编写configuration函数来在运行时将程序包挂接到彼此,而不是编译时间 。 可以导出 routes.Register ,这是main (或每个应用程序中的代码)可以调用的path,而不是导入定义路由的所有包的路由。 一般来说,configuration信息可能来自main或专用包。 你不希望它分散在整个应用程序。

  • 传递基本types和interface值。 如果你仅仅依赖于一个types名称的包,也许你可以避免这种情况。 也许一些处理[]Page代码可以取而代之使用[]string的文件名或ID的[]int或更通用的接口( sql.Rows )。

  • 考虑只有纯数据types和接口的“模式”包,所以User与可能从数据库加载用户的代码是分开的。 它不必依赖太多(可能是任何东西),所以你可以从任何地方包括它。 本·约翰逊(Ben Johnson)在GopherCon 2016上做了一个闪电般的演讲,build议并依靠依赖来组织软件包。

将代码组织成包:

  • 作为一个规则, 当每件作品可以独立使用时,将其分开 。 如果两个function是真正密切相关的,则根本不需要将它们拆分成多个包; 您可以使用多个文件或types进行组织。 大包可以, 例如,去net/http是一个。

  • 按主题或依赖关系分解抓包包( utilstools )。 否则,你最终可能会导入一个巨大的utils包(并承担所有的依赖关系)来实现一两个function(如果分离出来,那么这个function不会有太多的依赖关系)。

  • 考虑将可重用代码“向下”推送到从特定用例中解开的更低级别的包中。 如果你有一个包含你的内容pipe理系统的逻辑和全function的HTML操作代码的package page ,可以考虑将HTML的东西“向下”移动到一个package html这样你就可以在不导入不相关的内容pipe理的情况下使用它。


在这里,我将重新排列一些东西,使路由器不需要包含路由:相反,每个应用程序包都会调用一个router.Register()方法。 这就是Gorillanetworking工具包的mux软件包 。 您的routesdatabaseconstants包听起来像低层次的部分,应该由您的应用程序代码导入,而不是导入它。

通常,尝试在图层中构build您的应用程序。 您的高层次,特定于用例的应用程序代码应该导入更低层的,更基本的工具,而不是反过来。 这里有更多的想法:

  • 软件包用于分离独立可用的function位 ; 只要源文件变大,您就不需要分割一个。 与Python或Java不同的是,Go可以独立于软件包结构拆分,合并和重新排列文件,因此可以拆分大量文件而不会打乱软件包。

    标准库的net/http大约是7k行(计算注释/空白,但不是testing)。 在内部,它被分割成许多更小的文件和types。 但是这只是一个包,我认为这是因为没有理由让用户自行处理cookie。 另一方面, netnet/url 分开的,因为它们在HTTP之外使用。

    如果您可以将“实用工具”推入独立的库,并感觉像是自己的抛光产品,或者干净地将应用程序本身分层(例如,UI位于API顶部,位于某些核心库和数据模型的顶部),那就太好了。 同样,“水平”分离也许可以帮助您将应用程序放在脑海(例如,UI层分解为用户帐户pipe理,应用程序核心和pipe理工具,或者比这更细的东西)。 但是,核心观点是, 你可以自由分裂或不适合你

  • 设置API以在运行时configuration行为,所以您不必在编译时导入它。 因此,例如,您的URL路由器可以公开一个Register方法,而不是导入appAappB等,并appB读取一个var Routes 。 你可以做一个myapp/routes包,导入router和所有的意见,并调用router.Register 。 基本思想是路由器是不需要导入应用程序视图的通用代码。

    一些方法来放置configurationAPI:

    • 通过interface s或func传递应用行为: http可以传递Handler自定义实现(当然),也可以传递CookieJarFiletext/templatehtml/template可以接受可以从模板访问的function(在FuncMap )。

    • 在合适的情况下,从包中导出快捷方式函数:http ,调用者可以创build和单独configuration一些http.Server对象,或者调用使用全局Server http.ListenAndServe(...) 。 这给了你一个很好的devise – 一切都在一个对象中,调用者可以在一个进程中创build多个Server ,但是它提供了一种懒惰的方式来configuration简单的单服务器情况。

    • 如果你不得不把自己限制在超级优雅的configuration系统中,如果你不能适应你的应用程序:也许对于某些东西,一个package "myapp/conf" var Conf map[string]interface{}是有用的。 但要意识到全球机遇的不利因素。 如果你想写可重用的库,他们不能导入myapp/conf ; 他们需要接受他们在构造函数中所需要的所有信息。全局variables也会冒险布线,在一个假设中,当应用程序最终不会有某个值时, 也许今天你有一个单一的数据库configuration或HTTP服务器configuration或这样的,但有一天你没有。

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

  • 从应用程序依赖的基本任务。 我使用另一种语言编写的一个应用程序有一个“utils”模块,它将常规任务(例如,格式化date时间或使用HTML)与特定于应用程序的内容(取决于用户模式等)进行混合。 但是用户包导入了utils,创build了一个循环。 如果我正在移植到Go,我会将与用户相关的utils从“utils”模块中移出来,可能会使用用户代码,甚至超过它。

  • 考虑分手抓包袋。 稍微扩大一点:如果两个function是独立的(也就是说,如果你把一些代码移动到另一个软件包,那么这个function仍然是可行的), 而且从用户的angular度来看,这两个function是分开的。 有时绑定是无害的,但是有时会导致额外的依赖关系,或者一个不太通用的包名称只会使代码更清晰。 所以我的utils可能会被主题或依赖关系(例如, strutildbutil等) dbutil 。 如果你用这种方式包装了大量的软件包,我们已经有了一些goimports来帮助pipe理它们。

  • 用基本types和interfacereplaceAPI中的导入需求对象types。 说你的应用程序中的两个实体有一个多对多的关系,如UserGroup 。 如果他们生活在不同的包(一个大的“如果”),你不能有两个u.Groups()返回一个[]group.Group g.Users()g.Users()返回[]user.User因为这需要包到相互导入。

    但是,您可以更改其中一个或两个返回,例如[]uint ID或sql.Rows或其他可以获取到的interface ,而无需import特定的对象types。 根据你的用例,像UserGroup这样的types可能是密切相关的,所以最好把它们放在一个包中,但如果你认为它们应该是不同的,那么这是一种方法。

感谢详细的问题和后续。

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

与python相比,Golang在包pipe理方面要优越得多。 在Python中,你甚至可以dynamic地导入包。

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