修复 Go 中的导入周期

Fixing import cycle in Go

所以我有这个导入周期,我正试图解决这个问题。我有以下模式:

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包,反之亦然。我该如何解决 这个周期?有没有不同的方法来解决这个问题?

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

导入周期表明设计存在根本性错误。从广义上讲,您正在查看以下内容之一:

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

一般来说,您想要构建一个应用程序以便拥有三种基本类型的包:

  1. 完全独立的包,不引用其他第一方包(当然可以引用标准库或其他第三方包)。
  2. 仅内部依赖性为上述类型 1 的逻辑包,即完全独立的包。这些包不应相互依赖或依赖于以下类型 3 的包。
  3. "Wiring" 包,主要与逻辑包交互,处理实例化、初始化、配置和依赖注入。这些可以依赖于除其他类型 3 包之外的任何其他包。您应该需要非常非常少的这种类型的包 - 通常只需要一个,main,但对于更复杂的应用程序,偶尔需要两个或三个。

基本上,您可以通过引入接口并注入接口而不是结构来打破依赖关系。

以您的示例为例:

// 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 而不是传递 struct
  • struct View 正在实现这个接口

因此,包“action”不再需要导入包“view”,因为界面位于同一个包“action”中

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

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

// &{new name}