修复Go语言中的导入循环问题

16

我有一个导入循环需要解决,具体模式如下:

view/
- view.go
action/
- action.go
- register.go

总体思路是在视图上执行操作,并由视图执行:

// view.go
type View struct {
    Name string
}

// action.go
func ChangeName(v *view.View) {
    v.Name = "new name"
}

// register.go
const Register = map[string]func(v *view.View) {
    "ChangeName": ChangeName,
}

然后在view.go中,我们调用了这个:

func (v *View) doThings() {
    if action, exists := action.Register["ChangeName"]; exists {
        action(v)
    }
}

但这会导致循环,因为View依赖于Action包,反之亦然。我该如何解决这个循环?是否有一种不同的方法来处理这个问题?


这个回答解决了你的问题吗?不允许导入循环 - Michael Freidgeim
4个回答

28

导入循环表示设计上存在根本性的缺陷。广义而言,你可能会遇到以下情况:

  • 你混淆了职责。也许view根本不应该访问action.Register,或者action不应该负责更改视图名称(或同时两者都不应该)。这似乎是最有可能的情况。
  • 你在依赖具体实现,而应该依赖接口并注入具体实现。例如,视图不应直接访问action.Register,而应该调用view内定义的接口类型的方法,并在构造View对象时注入它。
  • 你需要一个或多个额外的、独立的包来保存viewaction两个包都使用的逻辑,但又不对它们进行调用。

一般来说,你希望架构应用程序,使其包分为三种基本类型:

  1. 完全自包含的包,不引用任何其他第一方包(当然可以引用标准库或其他第三方包)。
  2. 逻辑包,其唯一的内部依赖关系是类型1,即完全自包含的包。这些包不应互相依赖,也不应依赖下面类型3的包。
  3. “Wiring”包主要与逻辑包互动,并处理实例化、初始化、配置和依赖注入。这些包可以依赖于除了其他类型 3 包之外的任何其他包。您应该只需要非常少量的此类包 - 通常只有一个 main,但对于更复杂的应用程序可能需要两个或三个。

1
完美,这让我有些思考的食粮。 - flooblebit
1
我不小心给这个答案点了踩,现在我必须等待一段时间才能撤销我的错误。抱歉。 - Readren
1
我故意点了个踩,因为这个回答的态度让我不爽。这里唯一真正有问题的设计是Go编译器的。有很多其他语言在处理这个问题上做得更好,而且不会把责任推给开发者。顺便说一句,这个网站上充斥着那种说你做错了,你不可能比语言设计者更懂的回答;然而几年后,语言却实现了这个功能或者解决了这个问题。没必要为语言的缺点找借口。 - undefined
@YusufTarıkGünaydın,编译器是否有问题是一个看法问题,但在这个上下文中,这是一个基本上有缺陷的应用程序设计,因为它无法在其编写的语言中编译。有缺陷的设计也可以通过用允许导入循环的语言重写所有内容来修复,但这可能比只是打破循环更费力,而且似乎并不是一个特别有帮助的答案。 - undefined

3
基本上,您可以通过引入接口并注入接口而不是结构来打破依赖关系。例如,在您的示例中,它会像这样:
// view.go
package view

import "import_cycles/action"

type View struct {
    Name string
}

func (v *View) ModifyName(name string) {
    v.Name = name
}

func (v *View) DoThings() {
    if action, exists := action.Register["ChangeName"]; exists {
        action(v)
    }
}

// action.go
package action

func ChangeName(v NameChanger) {
    v.ModifyName("new name")
}

// register.go
package action

type NameChanger interface {
    ModifyName(name string)
}

var Register = map[string]func(v NameChanger){
    "ChangeName": ChangeName,
}

请注意,引入了NameChanger接口。以下是需要指出的内容:
  • 这个接口在函数ChangeName中进行注入,而不是传递结构体。
  • 结构体View正在实现此接口。

因此,包“action”不再需要导入包“view”,因为接口位于同一包“action”中。

在main.go中,我们可以测试结果:

v := &view.View{
    Name: "some name",
}
v.DoThings()
fmt.Println(v)

// &{new name}

2
在我的情况下,我在我的单元测试中创建了一个简单的导入循环。我的常规应用程序没有问题。

import cycle simple

回答你的关于修复问题的问题,首先我分离了导致“import cycle”问题的函数。在我的情况下,只有运行测试时才会出现“import cycle”。
然后我检查了import cycle的类型。这有助于可视化错误。我发现Package B的测试依赖于Package A。
我将测试移动到Package A中,这样就没有更多的“import cycle”[和更干净的测试]了。

0

导入循环是设计错误的结果。在两个方向上相互依赖的结构必须在同一个包中,否则会发生导入循环。顺便说一句,Go 不是唯一有这种限制的编程语言,例如 C++ 和 Python 也有。


实际上,我正在询问我的设计错误在哪里以及我该如何采用不同的方法来解决它。 - flooblebit
阅读我的回答的第二部分,那里有如何解决这个问题的方法。 - Bert Verhees
我知道我可以把它们放在同一个包里。但我更愿意创建一个更加周密的系统,以便将它们解耦,而不是把它们塞进同一个包中。 - flooblebit
没有其他办法,相互指向的结构体必须在同一个包中。 - Bert Verhees
9
顺便提一下,Go确实是唯一一个能够做到这一点的语言。在C++和Python中,你可以导入单个文件。这与必须导入整个包的情况有着巨大的不同。但也许这只是我来自其他语言的老思维方式。到目前为止,除了这个并不理想的解决方案,我还没有找到其他的解决方法。 - JamMaster
我不同意这个观点,如果你需要一个应用程序结构或上下文结构,其中引用了net包中的NetClientService和io包中的IOHandler,我们该怎么办?把所有东西都放在同一个包里吗?这对我来说似乎不正确。在Python和C++中,有一些解决这种无法避免的情况的方法(如前向声明,在Django中至少通过类名(字符串)进行引用)。 - Melardev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接