golang import cycle

项目结构变得复杂的时候,常常会遇到一个循环引用的问题,特别是对于Golang的项目,循环引用是被禁止的。这里简要罗列以下循环引用的起因以及出现循环引用之后应该如何解决。

交叉引用的解决

常常使用的一种方式是通过解耦合来达到消除循环引用的目的,在Golang中通过接口的方式进行接耦合是相当方便的。由于golang中struct是通过duck type的方式实现接口,比如一个struct实现了interfaceA以及interfaceB,那么在这个struct的定义文件中是不需要直接引用到interfaceA以及interfaceB这两个文件的,这里就实现了所谓的解耦合(从直接诶引用文件所在的目录到才用duck type的方式解除耦合)。下面看一个实际的例子,线不管这个例子的实际背景含义,比如一个parent package引用了children package,因为parent中定义了一个createchildren的方法,需要引用children的结构体定义,同时children package也需要引用parent package,因为Children需要记录下来到底是哪个Parent实例创建了它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package parent
import (
"fmt"
"child"
)
type Parent struct {
message string
}
func (parent *Parent) PrintMessage() {
fmt.Println(parent.message)
}
func (parent *Parent) CreateNewChild() *child.Child {
return child.NewChild(parent)
}
func NewParent() *Parent {
return &Parent{message: "Hello World"}
}

package child
import "Parent"
type Child struct {
parent *Parent
}
func (child *Child) PrintParentMessage() {
child.parent.PrintMessage()
}
func NewChild(parent *Parent) *Child {
return &Child{parent: parent }
}

此时就出现了交叉引用,按照之前说到的思路,将直接引用变成接口的引用。在Child实例中使用到parent的方法只有PrintParentMessage这个函数,将起抽象成一个interface,interface中定义了PrintParentMessage方法<这个接口的定义需要合child的文件放在一起>。原本Child中对于Parent的直接引用也变成了对于IParent的接口的引用.本质上来讲,child引用parent的功能仅仅是获取其中的某些信息,因此并不需要通过child完全得到parent,只需要通过child得到parent的一些方法,这些方法可以被定义在一个接口中,因此不必要把parent整个实例都暴露给child,仅仅是暴露出其中的一些方法即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
package child
type IParent interface {
PrintMessage()
}
type Child struct {
parent IParent
}
func (child *Child) PrintParentMessage() {
child.parent.PrintMessage()
}
func NewChild(parent *Parent) *Child {
return &Child{parent: parent }
}

于是整个流程如下,在创建child的时,parent实例被封装成为一个IParent interface实例存再了child的字段parent中,之后child再次调用的print方法的时候,通过接口实例去调用printmessage的方法,由于此时接口中存的value类型是Parent,因此会自动匹配到Parent struct所实现的PrintMessage方法进行调用,这样就实现了最初的消除交叉引用的目的。

此外,还有一种消除交叉引用的方式就是按照项目本身的逻辑层次来规划项目的目录,比如向这个例子中定义的Child以及Parent的struct,都可以提炼出来放在一个新的名为type.go的文件中,这个文件算是整个项目的最底层文件了,其他所有的Package可能都需要引用这个文件中定义的内容,这个文件不再需要引用其他的package了,因此也就消除了交叉引用。另外还有一些经验比如涉及到util相关的函数也都会独立出来放在一个新的文件中使用,这样也可以避免一些交叉引用的问题。

这里就涉及到了项目的结构组织的问题,这应该是个经验活儿,但是也应该有一些通用的原则和最佳实践,以下是通过阅读网上的相关资料整理的一部分。

项目的目录层次规划

比较好的方法论是这一篇(https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html),这里摘一些主要的内容:

  • 软件是分层次的,外部层次的代码依赖内部的层次代码,内部层次的代码不知道外部层次代码的行为,并且不对其产生依赖,一般最常见最靠近中心的内部层次就是某些抽象程度很高的接口的定义,以及用于抽象功能的一些struct定义 <struct本身并没有所谓的层次关系的说法,关键是看起所定义的内容是用于抽象功能的实现还是用于细节功能的实现>。
  • 层次越向内,逻辑的抽象程度越高,最内层的代码是抽象程度最高的也是最通用的代码,外层的代码是具体的实现细节。抽象代码不能依赖细节代码,细节代码应该依赖抽象代码。
    这样的方法对阅读源码来说也是有好处的,比如拿来一个新的项目,按照分层的逻辑去阅读就会清晰许多了。虽然说项目运行起来有一个数据流和功能步骤,但是项目在实现和设计的时候从实现的角度来讲都是按照这种层次化的关系来进行的,分析代码的时候从数据流的角度和项目实现的角度应该是两条线了。

相关参考

方法介绍

http://stackoverflow.com/questions/16168601/any-good-advice-about-how-to-avoid-import-cycle-in-go

https://en.wikipedia.org/wiki/Dependency_inversion_principle

https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html

http://stackoverflow.com/questions/1897537/why-are-circular-references-considered-harmful

实际的例子

http://manuel.kiessling.net/2012/09/28/applying-the-clean-architecture-to-go-applications/

http://mantish.com/post/dealing-with-import-cycle-go/

http://programminghave.blogspot.jp/2015/01/go-import-cycle-not-allowed.html

推荐文章