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
是一个。 -
按主题或依赖关系分解抓包包(
utils
,tools
)。 否则,你最终可能会导入一个巨大的utils
包(并承担所有的依赖关系)来实现一两个function(如果分离出来,那么这个function不会有太多的依赖关系)。 -
考虑将可重用代码“向下”推送到从特定用例中解开的更低级别的包中。 如果你有一个包含你的内容pipe理系统的逻辑和全function的HTML操作代码的
package page
,可以考虑将HTML的东西“向下”移动到一个package html
这样你就可以在不导入不相关的内容pipe理的情况下使用它。
在这里,我将重新排列一些东西,使路由器不需要包含路由:相反,每个应用程序包都会调用一个router.Register()
方法。 这就是Gorillanetworking工具包的mux
软件包 。 您的routes
, database
和constants
包听起来像低层次的部分,应该由您的应用程序代码导入,而不是导入它。
通常,尝试在图层中构build您的应用程序。 您的高层次,特定于用例的应用程序代码应该导入更低层的,更基本的工具,而不是反过来。 这里有更多的想法:
-
软件包用于分离独立可用的function位 ; 只要源文件变大,您就不需要分割一个。 与Python或Java不同的是,Go可以独立于软件包结构拆分,合并和重新排列文件,因此可以拆分大量文件而不会打乱软件包。
标准库的
net/http
大约是7k行(计算注释/空白,但不是testing)。 在内部,它被分割成许多更小的文件和types。 但是这只是一个包,我认为这是因为没有理由让用户自行处理cookie。 另一方面,net
和net/url
是分开的,因为它们在HTTP之外使用。如果您可以将“实用工具”推入独立的库,并感觉像是自己的抛光产品,或者干净地将应用程序本身分层(例如,UI位于API顶部,位于某些核心库和数据模型的顶部),那就太好了。 同样,“水平”分离也许可以帮助您将应用程序放在脑海(例如,UI层分解为用户帐户pipe理,应用程序核心和pipe理工具,或者比这更细的东西)。 但是,核心观点是, 你可以自由分裂或不适合你 。
-
设置API以在运行时configuration行为,所以您不必在编译时导入它。 因此,例如,您的URL路由器可以公开一个
Register
方法,而不是导入appA
,appB
等,并appB
读取一个var Routes
。 你可以做一个myapp/routes
包,导入router
和所有的意见,并调用router.Register
。 基本思想是路由器是不需要导入应用程序视图的通用代码。一些方法来放置configurationAPI:
-
通过
interface
s或func
传递应用行为:http
可以传递Handler
自定义实现(当然),也可以传递CookieJar
或File
。text/template
和html/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
可能会被主题或依赖关系(例如,strutil
,dbutil
等)dbutil
。 如果你用这种方式包装了大量的软件包,我们已经有了一些goimports
来帮助pipe理它们。 -
用基本types和
interface
replaceAPI中的导入需求对象types。 说你的应用程序中的两个实体有一个多对多的关系,如User
和Group
。 如果他们生活在不同的包(一个大的“如果”),你不能有两个u.Groups()
返回一个[]group.Group
g.Users()
和g.Users()
返回[]user.User
因为这需要包到相互导入。但是,您可以更改其中一个或两个返回,例如
[]uint
ID或sql.Rows
或其他可以获取到的interface
,而无需import
特定的对象types。 根据你的用例,像User
和Group
这样的types可能是密切相关的,所以最好把它们放在一个包中,但如果你认为它们应该是不同的,那么这是一种方法。
感谢详细的问题和后续。
基本上,你的代码是高度耦合的,Golang会强制你保持软件包的低耦合性,但是在一个包中,高内聚性是好的。
与python相比,Golang在包pipe理方面要优越得多。 在Python中,你甚至可以dynamic地导入包。
对于大型项目,golang将确保您的软件包更易于维护。