3 回答
TA贡献1834条经验 获得超8个赞
这有两个高级部分:确定哪些代码放在哪个包中,以及调整您的 API 以减少包承担尽可能多的依赖项的需要。
在设计无需某些导入的 API 时:
编写用于在运行时而不是编译时将包相互连接的配置函数。
routes
它可以导出 (或每个应用程序中的代码)可以调用的routes.Register
,而不是导入所有定义路由的包main
。一般来说,配置信息可能流经main
一个专用包;将它分散太多会使其难以管理。传递基本类型和
interface
值。如果您只依赖于一个类型名称的包,也许您可以避免这种情况。也许某些处理 a 的代码[]Page
可以使用 a[]string
of 文件名或 a[]int
of ID 或更通用的接口 (sql.Rows
) 来代替。考虑使用仅包含纯数据类型和接口的“模式”包,以便
User
与可能从数据库加载用户的代码分开。它不必依赖太多(可能依赖于任何东西),因此您可以从任何地方包含它。Ben Johnson 在 GopherCon 2016 上发表了一个闪电般的演讲,建议并按依赖项组织包。
将代码组织成包:
通常,当每个部分都可以单独使用时,将其拆分。如果两个功能确实密切相关,则根本不必将它们拆分为包;您可以改为使用多个文件或类型进行组织。大包也可以;
net/http
例如,Go就是其中之一。按主题或依赖项分解抓包包 (
utils
,tools
)。否则,您最终可能会utils
为一项或两项功能导入一个巨大的包(并承担其所有依赖项)(如果分开,则不会有这么多依赖项)。考虑将可重用代码“向下”推入与您的特定用例无关的较低级别的包中。 如果您有一个
package page
包含内容管理系统逻辑和通用 HTML 操作代码的代码,请考虑将 HTML 内容“向下”移动到 a,package html
这样您就可以使用它而无需导入不相关的内容管理内容。
在这里,我会重新安排一些东西,以便路由器不需要包含路由:相反,每个应用程序包都调用一个router.Register()
方法。这就是Gorilla Web 工具包的mux
包所做的。您的routes
、database
和constants
包听起来像是应该由您的应用程序代码导入而不是导入的低级片段。
通常,尝试分层构建您的应用程序。您的上层、特定于用例的应用程序代码应该导入下层、更基础的工具,而不是相反。这里还有一些想法:
包非常适合从调用者的角度分离独立可用的功能位。对于您的内部代码组织,您可以轻松地在包中的源文件之间打乱代码。您在中定义的符号的初始命名空间或只是 package ,根据需要拆分/连接文件并不难,尤其是在.
x/foo.go
x/bar.go
x
goimports
标准库
net/http
大约有 7k 行(包括注释/空白但不包括测试)。在内部,它被分成许多较小的文件和类型。但这是一个包,我认为因为用户没有理由想要,比如说,只是自己处理 cookie。另一方面,net
和net/url
是分开的,因为它们在 HTTP 之外使用。如果您可以将“向下”实用程序推送到独立的库中并且感觉像是他们自己的精美产品,或者干净地将应用程序本身分层(例如,UI 位于 API 之上,API 位于一些核心库和数据模型之上),那就太好了。同样,“横向”分离可以帮助您牢牢记住应用程序(例如,UI 层分解为用户帐户管理、应用程序核心和管理工具,或者比这更细粒度的东西)。但是,核心点是,您可以根据自己的情况自由拆分或不拆分。
设置 API 以在运行时配置行为,这样您就不必在编译时导入它。因此,举例来说,您的网址路由器可以公开一个
Register
方法,而不是进口appA
,appB
等等,读一var Routes
从每个。您可以制作一个myapp/routes
包来导入router
您的所有视图和调用router.Register
。基本思想是路由器是不需要导入应用程序视图的通用代码。将配置 API 组合在一起的一些方法:
通过
interface
s 或func
s传递应用程序行为: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{}
很有用。但请注意全局配置的缺点。如果你想编写可重用的库,它们不能导入myapp/conf
;他们需要接受他们在构造函数等中需要的所有信息。全局变量也有硬连接的风险,假设某些东西在应用程序范围内始终具有单一值,但最终不会;也许今天你有一个单一的数据库配置或 HTTP 服务器配置等,但有一天你没有。
移动代码或更改定义以减少依赖性问题的一些更具体的方法:
将基本任务与依赖于应用的任务分开。我使用另一种语言开发的一个应用程序有一个“utils”模块,它将一般任务(例如,格式化日期时间或使用 HTML)与特定于应用程序的内容(取决于用户架构等)混合在一起。但是 users 包导入了 utils,创建了一个循环。如果我要移植到 Go,我会将依赖于用户的 utils“向上”移出 utils 模块,可能是为了与用户代码一起使用,甚至在它之上。
考虑拆散手提包。稍微扩大最后一点:如果两个功能是独立的(也就是说,如果您将一些代码移到另一个包中,事情仍然有效)并且从用户的角度来看不相关,则它们是被分成两个包的候选者。有时捆绑是无害的,但有时它会导致额外的依赖关系,或者不那么通用的包名称只会使代码更清晰。所以我
utils
上面的内容可能会被主题或依赖项(例如strutil
,dbutil
, 等)分解。如果您以这种方式获得了大量包裹,我们必须goimports
帮助管理它们。将 API 中需要导入的对象类型替换为基本类型和
interface
s。假设您的应用程序中的两个实体具有多对多关系,例如User
s 和Group
s。如果它们位于不同的包中(一个很大的“if”),则不能同时u.Groups()
返回 a[]group.Group
和g.Users()
返回,[]user.User
因为这需要包相互导入。但是,您可以更改其中一个或两个返回值,例如,一个
[]uint
ID 或一个sql.Rows
或其他一些interface
您可以在不import
使用特定对象类型的情况下获得的返回值。根据您的用例,类型 likeUser
和Group
可能密切相关,最好将它们放在一个包中,但如果您决定它们应该不同,这是一种方法。
感谢您的详细问题和跟进。
TA贡献1943条经验 获得超7个赞
可能部分但丑陋的答案:与导入循环依赖问题斗争了一年。有一段时间,能够解耦足够多,因此没有导入周期。我的应用程序大量使用插件。同时,它使用编码/解码库(json 和 gob)。对于这些,我有自定义的 marshall 和 unmarshall 方法,以及 json 的等效方法。
为了使这些工作,包括包名称在内的完整类型名称在传递给编解码器的数据结构上必须相同。编解码器的创建必须在一个包中。这个包是从其他包和插件中调用的。只要编解码器包不需要调用任何调用它的包,或者使用方法或方法的接口,一切都可以工作。为了能够在插件中使用包中的类型,插件必须与包一起编译。由于我不想在插件的构建中包含主程序,这会破坏插件的重点,因此插件和主程序中只包含编解码器包。一切正常,直到我需要从编解码器包调用到主程序,在主程序调用编解码器包之后。这将导致导入周期。为了摆脱这种情况,我可以将编解码器放在主程序中而不是它自己的包中。但是,因为编组/解组方法中使用的特定数据类型在主程序和插件中必须相同,所以我需要使用每个插件的主程序包进行编译。此外,因为我需要主程序调用插件,所以我需要主程序中插件的接口类型。从来没有找到让它工作的方法,我确实想到了一个可能的解决方案:首先,将编解码器分成一个插件,而不仅仅是一个包然后,将它作为主程序中的第一个插件加载。创建注册函数以与底层方法交换接口。所有的编码器和解码器都是通过调用这个插件来创建的。插件通过注册的接口回调主程序。主程序和所有插件为此使用相同的接口类型包。然而,实际编码数据的数据类型在主程序中以不同的名称引用,但与插件中的底层类型相同,否则存在相同的导入循环。做这部分需要做一个不安全的演员。编写了一个强制转换的小函数,以便语法清晰:(<cast pointer type*>Cast(<pointer to structure, or interface to pointer to structure>)。实际编码数据的数据类型在主程序中使用不同的名称引用,但与插件中的底层类型相同,否则存在相同的导入循环。做这部分需要做一个不安全的演员。编写了一个强制转换的小函数,以便语法清晰:(<cast pointer type*>Cast(<pointer to structure, or interface to pointer to structure>)。实际编码数据的数据类型在主程序中使用不同的名称引用,但与插件中的底层类型相同,否则存在相同的导入循环。做这部分需要做一个不安全的演员。编写了一个强制转换的小函数,以便语法清晰:(<cast pointer type*>Cast(<pointer to structure, or interface to pointer to structure>)。
编解码器唯一的另一个问题是确保当数据发送到编码器时,它被强制转换,以便编组/解组方法识别数据类型名称。为了更容易,可以从一个包中导入主程序类型,从另一个包中导入插件类型,因为它们不相互引用。
非常复杂的解决方法,但不知道如何使这项工作。还没试过这个。一切都完成后,可能仍会以导入周期结束。
[更多关于这个]
为了避免导入循环问题,我使用了一种使用指针的不安全类型方法。首先,这是一个带有小函数 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 包中的类型不冲突如果您在插件之间使用公共包,则所有构建的插件和主程序的路径必须相同。使用plug包加载插件,交换函数指针
对于我的一个问题,我实际上是从主程序中的一个包调用一个插件,只是为了能够回调到主程序中的另一个包,避免两个包之间的导入循环。我使用带有自定义编组器方法的 json 和 gob 包遇到了这个问题。我在主程序和其他插件中使用自定义编组的类型,同时,我希望插件独立于主程序构建。我通过使用包含在主程序和插件中的 json 和 gob 编码/解码自定义方法包来实现这一点。但是,我需要能够从编码器方法回调到主程序,这给了我导入循环类型冲突。上述解决方案与另一个插件专门解决导入周期的工作原理。
希望这有助于解决这个问题。
TA贡献1796条经验 获得超4个赞
基本上你的代码是高度耦合的,Golang 强制你保持包低耦合,但在包内高内聚很好。
与 Python 相比,Golang 在包管理方面要优越得多。在 python 中,您甚至可以动态导入包。
对于大型项目,golang 将确保您的包更易于维护。
- 3 回答
- 0 关注
- 227 浏览
添加回答
举报