在Go中注册包时避免循环依赖的方法是:

huangapple go评论85阅读模式
英文:

Registering packages in Go without cyclic dependency

问题

我有一个中央包,提供了几个其他包依赖的接口(我们称之为Client)。这些其他包提供了这些接口的几个实现(UDPClientTCPClient)。我通过在中央包中调用NewClient来实例化一个Client,它会从其中一个依赖包中选择并调用适当的客户端实现。

当我想要告诉中央包关于这些其他包时,让它知道它可以创建哪些客户端时,这种方法就不起作用了。这些依赖的客户端实现也导入了中央包,创建了循环依赖,而Go不允许这样做。

前进的最佳方式是什么?我不想把所有这些实现都混在一个包中,创建一个单独的注册包似乎有点过度设计。目前,我让每个实现在中央包中注册自己,但这要求用户知道在每个使用客户端的独立二进制文件中导入每个实现。

import (
    _ udpclient
    _ tcpclient
    client
)
英文:

I have a central package that provides several interfaces that other packages are dependent on (let us call one Client). Those other packages, provide several implementations of those first interfaces (UDPClient, TCPClient). I instantiate a Client by calling NewClient in the central package, and it selects and invokes the appropriate client implementation from one of the dependent packages.

This falls apart when I want to tell the central package about those other packages, so it knows what clients it can create. Those dependent client implementations also import the central package, creating a cyclic dependency which Go does not allow.

What's the best way forward? I'd prefer not to mash all those implementations in a single package, and creating a separate registry package seems overkill. Currently I have each implementation register itself with the central package, but this requires that the user knows to import every implementation in every separate binary that makes use of client.

import (
    _ udpclient
    _ tcpclient
    client
)

答案1

得分: 33

标准库通过多种方式解决了这个问题:

1) 没有“中央”注册表

一个例子是不同的哈希算法。crypto 包只定义了 Hash 接口(类型和方法)。具体的实现在不同的包中(实际上是子文件夹,但不一定要这样),例如 crypto/md5crypto/sha256

当你需要一个“哈希器”时,你明确指定你想要的那个并实例化它,例如:

h1 := md5.New()
h2 := sha256.New()

这是最简单的解决方案,它也给你很好的分离:hash 包不需要知道或担心具体的实现。

如果你知道或可以决定你想要的实现,这是首选的解决方案。

2) 有一个“中央”注册表

这基本上是你提出的解决方案。实现必须以某种方式注册自己(通常在包的 init() 函数中)。

一个例子是 image 包。该包定义了 Image 接口及其几个实现。不同的图像格式在不同的包中定义,例如 image/gifimage/jpegimage/png

image 包有一个 Decode() 函数,它从指定的 io.Reader 解码并返回一个 Image。通常不知道读取器中的图像类型,因此无法使用特定图像格式的解码器算法。

在这种情况下,如果我们希望图像解码机制具有可扩展性,那么注册是不可避免的。最干净的方法是在包的 init() 函数中进行注册,这是通过在导入时为包名指定空白标识符来触发的。

请注意,这种解决方案还允许您使用特定的实现来解码图像,具体的实现也提供了 Decode() 函数,例如 png.Decode()


那么最好的方式是什么?

这取决于你的需求。如果你知道或可以决定你需要哪个实现,请选择 #1。如果你无法决定或不知道,并且需要可扩展性,请选择 #2。

...或者选择下面介绍的 #3。

3) 提出第三种解决方案:“自定义”注册表

你仍然可以拥有“中央”注册表的便利性,同时将接口和实现分离,但代价是“自动可扩展性”。

思路是在 pi 包中定义接口。在 papb 等包中实现。然后创建一个 pf 包,其中包含你想要的“工厂”方法,例如 pf.NewClient()pf 包可以引用 papbpi 包,而不会创建循环依赖关系。

英文:

The standard library solves this problem in multiple ways:

1) Without a "Central" Registry

Example of this is the different hash algorithms. The 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.

When you need a "hasher", you explicitly state which one you want and instantiate that one, e.g.

h1 := md5.New()
h2 := sha256.New()

This is the simplest solution and it also gives you good separation: the hash package does not have to know or worry about implementations.

This is the preferred solution if you know or you can decide which implementation you want prior.

2) With a "Central" Registry

This is basically your proposed solution. Implementations have to register themselves in some way (usually in a package init() function).

An example of this is the 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.

The image package has a Decode() function which decodes and returns an Image from the specified io.Reader. Often it is unknown what type of image comes from the reader and so you can't use the decoder algorithm of a specific image format.

In this case if we want the image decoding mechanism to be extensible, a registration is unavoidable. The cleanest to do this is in package init() functions which is triggered by specifying the blank identifier for the package name when importing.

Note that this solution also gives you the possibility to use a specific implementation to decode an image, the concrete implementations also provide the Decode() function, for example png.Decode().


So the best way?

Depends on what your requirements are. If you know or you can decide which implementation you need, go with #1. If you can't decide or you don't know and you need extensibility, go with #2.

...Or go with #3 presented below.

3) Proposing a 3rd Solution: "Custom" Registry

You can still have the convenience of the "central" registry with interface and implementations separated with the expense of "auto-extensibility".

The idea is that you have the interface in package pi. You have implementations in package pa, pb etc.

And you create a package pf which will have the "factory" methods you want, e.g. pf.NewClient(). The pf package can refer to packages pa, pb, pi without creating a circular dependency.

答案2

得分: 4

那些依赖的客户端实现也会导入中央包。

它们应该依赖于另一个定义它们需要依赖的接口的包(这些接口由第一个中央包实现)。

这通常是打破导入循环的方法(和/或使用依赖反转)。

在《Golang中的循环依赖和接口》中有更多描述的选项。

go list -f 也可以帮助可视化这些导入循环。

英文:

> Those dependent client implementations also import the central package

They should rely on another package defining interfaces that they need to rely on (and that are implemented by the first central package).

This is usually how an import cycle is broken (and/or using dependency inversion).

You have more options described in "Cyclic dependencies and interfaces in Golang".

go list -f can also help visualizing those import cycles.

huangapple
  • 本文由 发表于 2015年3月26日 13:05:20
  • 转载请务必保留本文链接:https://go.coder-hub.com/29271440.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定