在没有循环依赖的情况下在 Go 中注册包
Registering packages in Go without cyclic dependency
我有一个中央包,它提供了其他包所依赖的几个接口(我们称其中一个为 Client
)。那些其他的包,提供了那些第一个接口的几个实现(UDPClient
,TCPClient
)。我通过在中央包中调用 NewClient
来实例化一个 Client
,它从一个依赖包中选择并调用适当的客户端实现。
当我想告诉中央包有关其他包的信息时,这会崩溃,因此它知道它可以创建哪些客户端。这些依赖的客户端实现也导入中央包,创建 Go 不允许的循环依赖。
最好的前进方向是什么?我不希望将所有这些实现混在一个包中,创建一个单独的注册表包似乎有点过分了。目前我让每个实现都在中央包中注册自己,但这要求用户知道在每个使用客户端的单独二进制文件中导入每个实现。
import (
_ udpclient
_ tcpclient
client
)
Those dependent client implementations also import the central package
他们应该依赖另一个包来定义他们需要依赖的接口(并且由第一个中央包实现)。
这通常是导入周期中断的方式(and/or 使用 dependency inversion)。
“Cyclic dependencies and interfaces in Golang”中描述了更多选项。
go list -f
还可以帮助可视化这些导入周期。
标准库以多种方式解决了这个问题:
1) 没有 "Central" 注册表
这方面的例子是不同的哈希算法。 crypto
package just defines the Hash
interface (the type and its methods). Concrete implementations are in different packages (actually subfolders but doesn't need to be) for example crypto/md5
and crypto/sha256
.
当您需要 "hasher" 时,您可以明确说明您想要哪个并实例化那个,例如
h1 := md5.New()
h2 := sha256.New()
这是最简单的解决方案,它还为您提供了很好的分离:hash
包不必知道或担心实现。
如果您知道或者您可以先决定您想要哪种实施方式,那么这是首选解决方案。
2) 使用 "Central" 注册表
这基本上是您提出的解决方案。实现必须以某种方式注册自己(通常在包 init()
函数中)。
这方面的一个例子是 image
package. The package defines the Image
interface and several of its implementations. Different image formats are defined in different packages such as image/gif
, image/jpeg
and image/png
。
image
包有一个 Decode()
function which decodes and returns an Image
from the specified io.Reader
。通常不知道什么类型的图像来自 reader,因此您不能使用特定图像格式的解码器算法。
在这种情况下,如果我们希望图像解码机制具有可扩展性,则注册是不可避免的。最干净的做法是在包 init()
函数中,通过在导入时指定包名称的空白标识符来触发。
请注意,此解决方案还为您提供了使用特定实现来解码图像的可能性,具体实现还提供了 Decode()
功能,例如 png.Decode()
.
那么最好的方法是什么?
取决于您的要求。如果您知道或可以决定您需要哪种实现,请选择#1。如果您无法决定或不知道并且需要可扩展性,请选择#2。
...或者使用下面显示的#3。
3) 提出第三个解决方案:"Custom" 注册表
您仍然可以享受 "central" 注册表带来的便利,其中接口和实现被分开 "auto-extensibility"。
想法是您在包 pi
中有接口。您在包 pa
、pb
等
中有实现
然后您创建一个包 pf
,其中将包含您想要的 "factory" 方法,例如pf.NewClient()
。 pf
包可以引用包 pa
、pb
、pi
而不会产生循环依赖。
我有一个中央包,它提供了其他包所依赖的几个接口(我们称其中一个为 Client
)。那些其他的包,提供了那些第一个接口的几个实现(UDPClient
,TCPClient
)。我通过在中央包中调用 NewClient
来实例化一个 Client
,它从一个依赖包中选择并调用适当的客户端实现。
当我想告诉中央包有关其他包的信息时,这会崩溃,因此它知道它可以创建哪些客户端。这些依赖的客户端实现也导入中央包,创建 Go 不允许的循环依赖。
最好的前进方向是什么?我不希望将所有这些实现混在一个包中,创建一个单独的注册表包似乎有点过分了。目前我让每个实现都在中央包中注册自己,但这要求用户知道在每个使用客户端的单独二进制文件中导入每个实现。
import (
_ udpclient
_ tcpclient
client
)
Those dependent client implementations also import the central package
他们应该依赖另一个包来定义他们需要依赖的接口(并且由第一个中央包实现)。
这通常是导入周期中断的方式(and/or 使用 dependency inversion)。
“Cyclic dependencies and interfaces in Golang”中描述了更多选项。
go list -f
还可以帮助可视化这些导入周期。
标准库以多种方式解决了这个问题:
1) 没有 "Central" 注册表
这方面的例子是不同的哈希算法。 crypto
package just defines the Hash
interface (the type and its methods). Concrete implementations are in different packages (actually subfolders but doesn't need to be) for example crypto/md5
and crypto/sha256
.
当您需要 "hasher" 时,您可以明确说明您想要哪个并实例化那个,例如
h1 := md5.New()
h2 := sha256.New()
这是最简单的解决方案,它还为您提供了很好的分离:hash
包不必知道或担心实现。
如果您知道或者您可以先决定您想要哪种实施方式,那么这是首选解决方案。
2) 使用 "Central" 注册表
这基本上是您提出的解决方案。实现必须以某种方式注册自己(通常在包 init()
函数中)。
这方面的一个例子是 image
package. The package defines the Image
interface and several of its implementations. Different image formats are defined in different packages such as image/gif
, image/jpeg
and image/png
。
image
包有一个 Decode()
function which decodes and returns an Image
from the specified io.Reader
。通常不知道什么类型的图像来自 reader,因此您不能使用特定图像格式的解码器算法。
在这种情况下,如果我们希望图像解码机制具有可扩展性,则注册是不可避免的。最干净的做法是在包 init()
函数中,通过在导入时指定包名称的空白标识符来触发。
请注意,此解决方案还为您提供了使用特定实现来解码图像的可能性,具体实现还提供了 Decode()
功能,例如 png.Decode()
.
那么最好的方法是什么?
取决于您的要求。如果您知道或可以决定您需要哪种实现,请选择#1。如果您无法决定或不知道并且需要可扩展性,请选择#2。
...或者使用下面显示的#3。
3) 提出第三个解决方案:"Custom" 注册表
您仍然可以享受 "central" 注册表带来的便利,其中接口和实现被分开 "auto-extensibility"。
想法是您在包 pi
中有接口。您在包 pa
、pb
等
然后您创建一个包 pf
,其中将包含您想要的 "factory" 方法,例如pf.NewClient()
。 pf
包可以引用包 pa
、pb
、pi
而不会产生循环依赖。