我是一个很长时间的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
Run Code Online (Sandbox Code Playgroud)
settings/database.py具有通用功能connect(),可以打开数据库会话.因此,应用程序包调用中的应用程序将database.connect()打开一个数据库会话.
同样的情况是settings/routes.py它具有允许应用程序将其子路由添加到主路由对象的功能.
设置包更多地是关于函数而不是数据/常量.这包含应用程序包中的应用程序使用的代码,否则必须在所有应用程序中复制这些代码.因此,如果我需要更改路由器类,我只需要更改settings/router.py,应用程序将继续工作而不进行任何修改.
two*_*two 69
这有两个高级部分:确定哪个代码包含在哪个包中,并调整API以减少对包依赖的需求.
在设计避免某些导入的API时:
编写配置函数,用于在运行时将程序包连接到彼此而不是编译时.routes它可以导出 routes.Register,而不是导入定义路由的所有包,而不是main(或每个应用程序中的代码)可以调用.通常,配置信息可能来自main或专用包; 你不希望它分散在你的应用程序中.
传递基本类型和interface值.如果你只依赖于一个类型名称的包,也许你可以避免这种情况.也许某些处理a的代码[]Page可以改为使用[]string文件名或[]intID或更通用的接口(sql.Rows)代替.
考虑使用仅具有纯数据类型和接口的"模式"包,因此User与可能从数据库加载用户的代码分开.它不必依赖很多(可能是任何东西),所以你可以从任何地方包含它.Ben Johnson在2016年的GopherCon上发表了闪电般的演讲,建议并按照依赖关系组织软件包.
将代码组织到包中:
作为一项规则,当每个部分本身有用时,拆分包.如果两个功能真正密切相关,那么您根本不必将它们拆分成包; 您可以组织多个文件或类型.大包装可以; net/http例如,Go 是一个.
按主题或依赖关系分解抓包包(utils,tools).否则,您最终可能导入一个巨大的utils包(并承担其所有依赖关系)以获得一个或两个功能(如果分离出来,则不会有如此多的依赖关系).
考虑将可重用代码"down"推送到与您的特定用例无关的较低级别的包中. 如果您package page的内容管理系统和通用HTML操作代码都包含逻辑,请考虑将HTML内容"向下"移动到一个,package html这样您就可以使用它而无需导入不相关的内容管理内容.
在这里,我重新安排了一些东西,所以路由器不需要包含路由:相反,每个app包调用一个router.Register()方法.这就是Gorilla Web工具mux包的功能.您的routes,database和constants包听起来像应该由应用代码导入的低级部分,而不是导入它.
通常,尝试在图层中构建您的应用.您的更高层,特定于用例的应用程序代码应该导入更低层,更基础的工具,而不是相反.以下是一些想法:
从调用者的角度来看,包适用于分离独立可用的功能位.对于内部代码组织,您可以轻松地在包中的源文件之间混洗代码.您定义的符号的初始命名空间或仅仅是包,并且根据需要分割/连接文件并不困难,尤其是在诸如此类实用程序的帮助下.x/foo.gox/bar.goxgoimports
标准库net/http大约是7k行(计数注释/空白但不测试).在内部,它被分成许多较小的文件和类型.但它是一个包,我认为'因为没有理由用户会想要,比如说,只是自己处理cookie.在另一方面,net并且net/url 是独立的,因为他们有外界HTTP使用.
如果您可以将"向下"实用程序推送到独立的库中并且感觉像是他们自己的优质产品,或者干净地将应用程序本身分层(例如,UI位于API顶部,位于某些核心库和数据模型之上),那就太棒了.同样,"水平"分离可以帮助您掌握应用程序(例如,UI层分为用户帐户管理,应用程序核心和管理工具,或者比这更细粒度的东西).但是,核心问题是,你可以自由地分裂或不适合你.
设置API以在运行时配置行为,因此您不必在编译时导入它.因此,举例来说,您的网址路由器可以公开一个Register方法,而不是进口appA,appB等等,读一var Routes从每个.您可以创建一个myapp/routes导入的包router以及所有视图和调用router.Register.基本思想是路由器是无需导入应用程序视图的通用代码.
将API配置在一起的一些方法:
通过interfaces或funcs 传递应用程序行为: http可以传递Handler(当然)的自定义实现,但也CookieJar可以File.text/template并且html/template可以接受可从模板访问的功能(在a中FuncMap).
如果适用http,从包中导出快捷方式功能:在,调用者可以制作和单独配置某些http.Server对象,也http.ListenAndServe(...)可以使用全局调用Server.这给了你一个很好的设计 - 一个对象中的所有内容和调用者可以Server在一个进程中创建多个s等 - 但它也提供了一种在简单的单服务器情况下配置的懒惰方式.
如果你不得不这样做,只需用胶带粘贴它:你不必限制自己超级优雅的配置系统,如果你不适合你的应用程序:可能对某些东西package "myapp/conf",全局var Conf map[string]interface{}是有用的.但要注意全球conf的缺点.如果要编写可重用的库,则无法导入myapp/conf; 他们需要接受构造函数中所需的所有信息等.Globalbals还冒着硬连接的风险,假设某些东西在应用程序范围内总是有一个值,而最终不会; 也许今天你有一个数据库配置或HTTP服务器配置等,但总有一天你没有.
一些更具体的方法来移动代码或更改定义以减少依赖性问题:
将基本任务与应用程序相关的任务分开.我在另一种语言中使用的一个应用程序有一个"utils"模块,它将一般任务(例如,格式化日期时间或使用HTML)与特定于应用程序的内容(取决于用户架构等)混合在一起.但是用户包导入了utils,创建了一个循环.如果我移植到Go,我会将用户相关的utils"up"移出utils模块,可能会使用用户代码甚至高于它.
考虑分解抓包装.稍微扩大最后一点:如果两个功能是独立的(即,如果将一些代码移动到另一个包,事情仍然有效)并且从用户的角度来看是无关的,那么它们可以被分成两个包.有时捆绑是无害的,但有时它会导致额外的依赖性,或者一个不太通用的包名称只会产生更清晰的代码.所以我utils上面可能会按主题或依赖(例如,被打破了strutil,dbutil等).如果您以这种方式结束大量的包裹,我们必须goimports帮助管理它们.
用基本类型和interfaces 替换API中的需要导入的对象类型.假设您的应用中的两个实体具有多对多关系,例如Users和Groups.如果它们存在于不同的包中(大'if'),则不能同时u.Groups()返回[]group.Group和g.Users()返回,[]user.User因为这需要包相互导入.
但是,如果没有特定的对象类型,您可以更改其中一个或两个,例如,一个[]uintID或一个sql.Rows或其他一些.根据您的使用案例,类型和类型可能如此密切相关,以便将它们放在一个包中更好,但如果您认为它们应该是不同的,那么这是一种方式.interfaceimportUserGroup
感谢您提供详细的问题和跟进.