Golang中的循环依赖和接口 [英] Cyclic dependencies and interfaces in Golang
问题描述
我是一个很长时间的Python开发者。我正在尝试Go,将现有的Python应用程序转换为Go。它是模块化的,对我来说效果非常好。
在Go中创建相同的结构后,我似乎陷入循环导入错误,比我想要的要多得多。 python中从来没有任何导入问题。我甚至从来不需要使用导入别名。所以我可能有一些循环进口,这在python中并不明显。我其实觉得很奇怪。
无论如何,我迷路了,试图在Go中解决这些问题。我已经读过,接口可以用来避免循环依赖。但我不明白如何。我也没有在这方面找到任何例子。有人可以帮我解决这个问题吗?
当前的python应用程序结构如下:
/main.py
/settings/routes.py包含主要路线取决于app1 / routes.py,app2 / routes.py等
/settings/database.py像connect()这样打开数据库会话
/settings/constants.py常规常量
/apps/app1/views.py url处理函数
/ apps / app1 / models .py应用程序特定的数据库函数取决于settings / database.py
/apps/app1/routes.py特定应用程序的路线
/apps/app2/views.py url处理函数
/apps/app2/models.py应用程序特定的数据库函数取决于设置/ database.py
/apps/app2/routes.py应用程序特定的路线
$ b settings / database.py
具有通用函数,如 connect()
它打开一个数据库会话。因此,应用程序包中的应用程序会调用 database.connect()
并打开db会话。
设置包更多的是关于函数而不是数据/常量。这包含应用程序包中的应用程序使用的代码,否则这些代码必须在所有应用程序中重复使用。因此,如果我需要更改路由器类,例如,我只需要更改 有两个高层次的内容:找出哪些代码放在哪个包中,并调整API以减少软件包承担尽可能多的依赖性的需要。 在设计API时避免需要进行一些导入: 编写配置函数,以便在运行时将包彼此挂钩,而不是编译时间。它可以导出 通常情况下,当每件作品可以自行使用时,将包分开。如果两个功能是真正密切相关的,则根本不需要将它们拆分成包;您可以使用多个文件或类型进行组织。大包装可以;例如,Go的
settings / router.py
,这些应用程序将继续工作而不做任何修改。
routes.Register
路径,而不是 routes
>导入所有定义路由的包, ,其中 main
(或每个应用中的代码)都可以调用。一般来说,配置信息可能来自 main
或专用包;您不希望它散布在您的应用程序中。 [] Page
的代码可以使用 []字符串
的文件名或<$ c $代替ID或一些更通用的接口( sql.Rows
)的c> [] int 。
net / http
是其中之一。 utils
, tools
)按主题或依赖关系。否则,您最终可能导入巨大的 utils
包(并承担其所有依赖),以实现一个或两个功能(如果分离出来,则不会有太多的依赖关系)。
考虑将可重用代码'向下'推送到从特定用例中解开的低级包中。如果您有 在这里,我会重新安排一些东西,让路由器不会' t需要包含路由:相反,每个应用程序包都会调用 通常,尝试在图层中构建您的应用程序。您的高层次,特定于用例的应用程序代码应该导入更低层的,更基本的工具,而不是其他方式。这里有更多的想法: 软件包用于分离独立可用的功能位;只要源文件变大,您就不需要分割一个。与Python或Java不同,Go可以独立于软件包结构拆分,合并和重新排列文件,因此您可以拆分大量文件而不会打乱软件包。标准库的 如果您可以将实用工具压入到独立的库中,并感觉像是他们自己的抛光产品或干净的图层您的应用程序本身(例如,UI位于API的顶部,位于某些核心库和数据模型的顶部)。同样,横向分离可以帮助您掌握应用程序(例如,UI层分成用户帐户管理,应用程序核心和管理工具,或者比这更细的东西)。但是,核心要点是,您可以自由分配或不为您工作。 将配置API整合在一起的一些方法: 通过 如果适用,从包中导出快捷方式函数: > http 包页面 code>包含你的内容管理系统的逻辑和全功能的HTML操作代码,考虑把HTML内容向下移动到
,调用者可以创建并单独配置一些包html
,这样你就可以使用它而不导入不相关的内容管理内容。
$ b router.Register()
方法。这是 Gorilla网络工具包的 mux
包确实。您的路线
,数据库
和常量
应该由您的应用程序代码导入并且不导入它的级别块。
net / http
约为7k行(计算注释/空白,但不包括测试)。在内部,它分成许多较小的文件和类型。但我认为这是一个包,因为没有理由让用户想要自己处理cookie。另一方面, net
和 net / url
是,因为它们在外部使用HTTP。
Register
方法而不是导入 appA
, appB
等,并读取 var Routes
来自每个。你可以制作一个 myapp / routes
软件包,它可以导入路由器
以及所有视图和调用路由器.Register
。基本思想是,路由器是不需要导入应用程序视图的通用代码。
界面
s或 func
s: http
可以通过 Handler
的自定义实现(当然)还包括 CookieJar
或文件
。 text / template
和 html / template
可以接受可以从模板访问的函数(在 FuncMap
)。
http.Server
对象,或者调用 http.ListenAndServe(.. 。)
使用全局服务器
。这给了你一个很好的设计 - 一切都在一个对象中,调用者可以在一个过程中创建多个 Server
s,但它也提供 提供在简单的单服务器情况下配置的一种懒惰方式。
包含全局 var conf map [字符串]的myapp / conf
界面{}
很有用。 但请注意全球机构的不利因素。如果你想编写可重用的库,它们不能导入
myapp / conf
;他们需要接受他们在构造函数中需要的所有信息等。全局变量也存在硬编码风险,假设某些事情总是在应用程序范围内具有单一值,而最终不会;也许今天你有一个单一的数据库配置或HTTP服务器配置等,但有一天你不会。 一些更具体的移动代码或更改定义以减少依赖性问题的方法: 将基本任务从与应用相关的任务中分离出来。我使用另一种语言开发的一个应用具有一个utils模块,用于将常规任务(例如,格式化日期时间或使用HTML)这取决于用户架构等)。但是用户软件包导入了utils,创建了一个循环。如果我正在移植到Go,我会将用户相关的utils从utils模块中移出,可能会与用户代码一起居住,甚至超出它。
< 稍微扩大一点:如果两个功能是独立的(即,如果您将某些代码移动到另一个软件包中,这些功能仍然有效) 和与用户的角度无关,他们是候选人被分为两个包。有时捆绑是无害的,但有时会导致额外的依赖关系,或者一个不太通用的包名称只会产生更清晰的代码。因此,上面的 utils
可能会被主题或依赖关系(例如, strutil
, dbutil
等)。如果你用这种方式包装了很多软件包,我们已经有了 goimports $ c $使用基本类型和
替换API中的需要导入的对象类型。 接口
s 说你的应用程序中的两个实体具有多对多的关系,如 User
s和组
秒。如果他们生活在不同的包中(一个很大的'如果'),你不能同时返回一个 []组
和 u.Groups()
。 Group g.Users()
返回 [] user.User
,因为这需要包相互导入。然而,你可以改变其中的一个或两个,例如一个 [] uint
的ID或一个 sql.Rows
或其他一些接口
您可以在 User
和 Group
这样的类型可能如此密切相关,所以最好放置它们一个包,但如果你决定他们应该是不同的,这是一种方式。
感谢您的详细问题和跟进。
I am a long time python developer. I was trying out Go, converting an existing python app to Go. It is modular and works really well for me.
Upon creating the same structure in Go, I seem to land in cyclic import errors, a lot more than I want to. Never had any import problems in python. I never even had to use import aliases. So I may have had some cyclic imports which were not evident in python. I actually find that strange.
Anyways, I am lost, trying to fix these in Go. I have read that interfaces can be used to avoid cyclic dependencies. But I don't understand how. I didn't find any examples on this either. Can somebody help me on this?
The current python application structure is as follows:
/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
has generic functions like connect()
which opens a db session. So an app in the apps package calls database.connect()
and a db session is opened.
The same is the case with settings/routes.py
it has functions that allow apps to add their sub-routes to the main route object.
The settings package is more about functions than data/constants. This contains code that is used by apps in the apps package, that would otherwise have to be duplicated in all the apps. So if I need to change the router class, for instance, I just have to change settings/router.py
and the apps will continue to work with no modifications.
There're two high-level pieces to this: figuring out which code goes in which package, and tweaking your APIs to reduce the need for packages to take on as many dependencies.
On designing APIs that avoid the need for some imports:
Write config functions for hooking packages up to each other at run time rather than compile time. Instead of
routes
importing all the packages that define routes, it can exportroutes.Register
, whichmain
(or code in each app) can call. In general, configuration info probably flows frommain
or a dedicated package; you don't want it scattered throughout your app.Pass around basic types and
interface
values. If you're depending on a package for just a type name, maybe you can avoid that. Maybe some code handling a[]Page
can get instead use a[]string
of filenames or a[]int
of IDs or some more general interface (sql.Rows
) instead.Consider having 'schema' packages with just pure data types and interfaces, so
User
is separate from code that might load users from the database. It doesn't have to depend on much (maybe on anything), so you can include it from anywhere. Ben Johnson gave a lightning talk at GopherCon 2016 suggesting that and organizing packages by dependencies.
On organizing code into packages:
As a rule, split a package up when each piece could be useful on its own. If two pieces of functionality are really intimately related, you don't have to split them into packages at all; you can organize with multiple files or types instead. Big packages can be OK; Go's
net/http
is one, for instance.Break up grab-bag packages (
utils
,tools
) by topic or dependency. Otherwise you can end up importing a hugeutils
package (and taking on all its dependencies) for one or two pieces of functionality (that wouldn't have so many dependencies if separated out).Consider pushing reusable code 'down' into lower-level packages untangled from your particular use case. If you have a
package page
containing both logic for your content management system and all-purpose HTML-manipulation code, consider moving the HTML stuff "down" to apackage html
so you can use it without importing unrelated content management stuff.
Here, I'd rearrange things so the router doesn't need to include the routes: instead, each app package calls a router.Register()
method. This is what the Gorilla web toolkit's mux
package does. Your routes
, database
, and constants
packages sound like low-level pieces that should be imported by your app code and not import it.
Generally, try to build your app in layers. Your higher-layer, use-case-specific app code should import lower-layer, more fundamental tools, and never the other way around. Here are some more thoughts:
Packages are for separating independently usable bits of functionality; you don't need to split one off whenever a source file gets large. Unlike in, say, Python or Java, in Go one can split and combine and rearrange files completely independent of the package structure, so you can break up huge files without breaking up packages.
The standard library's
net/http
is about 7k lines (counting comments/blanks but not tests). Internally, it's split into many smaller files and types. But it's one package, I think 'cause there was no reason users would want, say, just cookie handling on its own. On the other hand,net
andnet/url
are separate because they have uses outside HTTP.It's great if you can push "down" utilities into libraries that are independent and feel like their own polished products, or cleanly layer your application itself (e.g., UI sits atop an API sits atop some core libraries and data models). Likewise "horizontal" separation may help you hold the app in your head (e.g., the UI layer breaks up into user account management, the application core, and administrative tools, or something finer-grained than that). But, the core point is, you're free to split or not as works for you.
Set up APIs to configure behavior at run-time so you don't have to import it at compile time. So, for example, your URL router can expose a
Register
method instead of importingappA
,appB
, etc. and reading avar Routes
from each. You could make amyapp/routes
package that importsrouter
and all your views and callsrouter.Register
. The fundamental idea is that the router is all-purpose code that needn't import your application's views.Some ways to put together config APIs:
Pass app behavior via
interface
s orfunc
s:http
can be passed custom implementations ofHandler
(of course) but alsoCookieJar
orFile
.text/template
andhtml/template
can accept functions to be accessible from templates (in aFuncMap
).Export shortcut functions from your package if appropriate: In
http
, callers can either make and separately configure somehttp.Server
objects, or callhttp.ListenAndServe(...)
that uses a globalServer
. That gives you a nice design--everything's in an object and callers can create multipleServer
s in a process and such--but it also offers a lazy way to configure in the simple single-server case.If you have to, just duct-tape it: You don't have to limit yourself to super-elegant config systems if you can't fit one to your app: maybe for some stuff a
package "myapp/conf"
with a globalvar Conf map[string]interface{}
is useful. But be aware of downsides to global conf. If you want to write reusable libraries, they can't importmyapp/conf
; they need to accept all the info they need in constructors, etc. Globals also risk hard-wiring in an assumption something will always have a single value app-wide when it eventually won't; maybe today you have a single database config or HTTP server config or such, but someday you don't.
Some more specific ways to move code or change definitions to reduce dependency issues:
Separate fundamental tasks from app-dependent ones. One app I work on in another language has a "utils" module mixing general tasks (e.g., formatting datetimes or working with HTML) with app-specific stuff (that depends on the user schema, etc.). But the users package imports the utils, creating a cycle. If I were porting to Go, I'd move the user-dependent utils "up" out of the utils module, maybe to live with the user code or even above it.
Consider breaking up grab-bag packages. Slightly enlarging on the last point: if two pieces of functionality are independent (that is, things still work if you move some code to another package) and unrelated from the user's perspective, they're candidates to be separated into two packages. Sometimes the bundling is harmless, but other times it leads to extra dependencies, or a less generic package name would just make clearer code. So my
utils
above might be broken up by topic or dependency (e.g.,strutil
,dbutil
, etc.). If you wind up with lots of packages this way, we've gotgoimports
to help manage them.Replace import-requiring object types in APIs with basic types and
interface
s. Say two entities in your app have a many-to-many relationship likeUser
s andGroup
s. If they live in different packages (a big 'if'), you can't have bothu.Groups()
returning a[]group.Group
andg.Users()
returning[]user.User
because that requires the packages to import each other.However, you could change one or both of those return, say, a
[]uint
of IDs or asql.Rows
or some otherinterface
you can get to withoutimport
ing a specific object type. Depending on your use case, types likeUser
andGroup
might be so intimately related that it's better just to put them in one package, but if you decide they should be distinct, this is a way.
Thanks for the detailed question and followup.
这篇关于Golang中的循环依赖和接口的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!