• Go中的循环依赖产生的原因及正确的处理之道
  • 发布于 2个月前
  • 112 热度
    0 评论
作为一个 Golang 开发,你可能在项目中遇到过包的循环依赖问题。Golang 不允许循环依赖,如果检测到代码中存在这种情况,在编译时就会抛出异常。本文会讨论循环依赖是如何发生的以及如何处理。

循环依赖
假设我们有两个包:p1和p2。当包p1依赖包p2,包p2依赖包p1时,就会产生循环依赖。真实情况可能会更复杂一些。例如,包p2不直接依赖包p1而是依赖于包p3,而p3又依赖包p1,这就构成了循环依赖。

下面来看两个包互相依赖的示例:
Package p1:
package p1
import (
 "fmt"
 "import-cycle-example/p2"
)

type PP1 struct{}

func New() *PP1 {
 return &PP1{}
}
func (p *PP1) HelloFromP1() {
 fmt.Println("Hello from package p1")
}
func (p *PP1) HelloFromP2Side() {
 pp2 := p2.New()
 pp2.HelloFromP2()
}
Package p2:
package p2
import (
 "fmt"
 "import-cycle-example/p1"
)

type PP2 struct{}

func New() *PP2 {
 return &PP2{}
}

func (p *PP2) HelloFromP2() {
 fmt.Println("Hello from package p2")
}

func (p *PP2) HelloFromP1Side() {
 pp1 := p1.New()
 pp1.HelloFromP1()
}
执行go build, 编译器会返回这样的错误:
imports import-cycle-example/p1
imports import-cycle-example/p2
imports import-cycle-example/p1: import cycle not allowed

调试循环依赖
比较尴尬的是Go语言并不会告诉你循环依赖导致错误的源文件或者源码信息。因此当你的代码库很大时,定位这个问题就有点困难。你可能会在多个不同的文件或包里徘徊,检查问题出在哪里。为什么Go中不显示导致错误的原因呢?原因是在循环依赖中并不是只有一个源文件。但Go语言会在报错信息中告诉你导致问题的package名,因此可以通过包名来解决问题。

也可以使用godepgraph工具, 把项目中包之间的依赖关系可视化,可以通过这个指令进行安装:
go get github.com/kisielk/godepgraph
它会以 Graphviz 点格式展示依赖图。如果你安装了graphviz工具(没有的话可以通过这个链接下载),你可以通过管道命令输出dot格式来渲染依赖图。
godepgraph -s import-cycle-example | dot -Tpng -o godepgraph.png
可以在输出的png图中查看到依赖关系:

除了godepgraph,你还可以使用go list命令得到一些启发(运行go help list命令来获取额外的信息)。
go list -f '{\{join .DepsErrors "\n"\}}' <import-path>
你可以提供引用路径,也可以对当前目录留空。

解决循环依赖问题
当你遇到循环依赖问题时,先思考项目的组织关系是否合理。处理循环依赖最常见的方法是interface,但有时你可能并不需要它。检查一下产生循环依赖关系的包,如果他们之间强耦合,需要通过互相引用对方来工作,那它们可能需要合并成一个包。在Go中,包是一个编译单元,如果两个包需要一起编译,他们应该处于相同的包下。

用interface解决循环依赖
.包p1通过导入p2来使用p2的函数/变量。
.包p2不想导入p1包,但是要使用p1包的函数/变量,可以在p2中声明p1的接口,然后通过对象实例来调用接口,这些对象会被视为包p2的对象。
这样包p2不用导入包p1,循环依赖被打破。p2包的代码如下:
package p2

import (
 "fmt"
)

type pp1 interface {
 HelloFromP1()
}

type PP2 struct {
 PP1 pp1
}

func New(pp1 pp1) *PP2 {
 return &PP2{
  PP1: pp1,
 }
}

func (p *PP2) HelloFromP2() {
 fmt.Println("Hello from package p2")
}

func (p *PP2) HelloFromP1Side() {
 p.PP1.HelloFromP1()
}
p1包的代码如下:
package p1

import (
 "fmt"
 "import-cycle-example/p2"
)

type PP1 struct{}

func New() *PP1 {
 return &PP1{}
}

func (p *PP1) HelloFromP1() {
 fmt.Println("Hello from package p1")
}

func (p *PP1) HelloFromP2Side() {
 pp2 := p2.New(p)
 pp2.HelloFromP2()
}
main包的调用关系如下:
package main
import (
 "import-cycle-example/p1"
)

func main() {
 pp1 := p1.PP1{}
 pp1.HelloFromP2Side() // Prints: "Hello from package p2"
}
你可以在GitHub中找到源文件:jogendra/import-cycle-example-go。

另一种使用接口解决循环依赖的方法是将接口代码作为独立桥梁放到独立的第三方包中。但很多时候它增加了代码的重复性,要使用这种方法的话需要牢记你的代码结构
"三包"调用链:包p1 -> 包m2 & 包p2 -> 包m1.

结语
当你的代码库很大时,循环依赖问题肯定非常痛苦。所以需要尝试分层构建应用程序,高层应该导入低层,而低层不应导入高层(会导致循环依赖)。需要记住:强耦合的包可以合并成一个,这样比通过interface解决依赖循环更好,但对于一般情况,一般需要通过interface来解决循环依赖。
用户评论