最近开发项目的时候,遇到了一个go里面很不好解决的问题,循环引用。

为了方便展示,我将整个项目抽象了下,项目结果如下 structure 项目的错误如下 error 包A中的代码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package A

import (
	"strings"
	"github.com/hundred666/GoTest/B"
)

func Foo(a string) (string) {
	return B.Add(a)
}

func Minus(a string) (string) {
	return strings.Trim(a, "\t")
}

包B中的代码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package B

import "github.com/hundred666/GoTest/A"

func Goo(a string) (string) {
	return A.Minus(a)
}

func Add(a string) (string) {
	return a + "----"
}

主函数代码如下

1
2
3
4
5
6
7
8
9
package main

import (
	"github.com/hundred666/GoTest/A"
)

func main() {
	A.Foo("good")
}

就是单纯的用了两个包里面的函数,就不让用! 在网上搜解决方案,主要有以下几个办法

设计整个项目结构

在写这些代码之前,就设计好项目的结构,避免出现这些情况。 这个是最应该做的办法,可是代码都写了这么多了再从头写一遍很不合理,而且这种需要一个对整个项目把控能力很强的架构师来做,我还到不了这么高的水平。

引入第三方包

我们可以看到,循环依赖只是对立面一些函数的依赖,所以,如果我们将这些函数单独抽象出去的话,也是一种解决办法。大体的文档结构就是

1
2
3
4
5
6
7
8
9
main

A +------------>Foo

B +------------>Goo

C +------------->Add()
  |
  +------------->Minus()

就是将A,B包里面的共用函数单独封成第三个包,A,B调用C的包 不可否认,这个方法会解决一些问题,可是肯定不是最优的解决办法。 因为我们这的函数比较简单,如果加减函数需要使用包里面的一些变量,可如何解决? 所以这个方法也不是非常合适。

函数作为参数传递

这个是一个比较有意思的解决方案,我们可以看到A中的Foo函数引用了B中的Add函数,我们可以将Add函数作为参数传递给A,A再进行调用

1
2
3
func Foo(a string, f func(string)(string))(string){
	return f(a)
}

main里面需要改成这样

1
r := A.Foo("good", B.Add)

运行结果正确,问题得解。 可是这样的话,就把难度全部放到了调用的函数那,依然具有很高的耦合度,不利于项目的开发,还有没有其他的解决办法?

使用外观模式

我们之前的java设计模式中介绍到了外观模式,发现这在很有用 我首先将包A,B中的方法抽象成接口,将方法先隔离出来

1
2
3
4
5
6
7
8
9
package service

type A interface {
	Minus(s string) (string)
}

type B interface {
	Add(s string) (string)
}

然后我A,B实现接口。为了容易处理,我不放定义两个结构体进行处理。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package A

import (
	"strings"
	"github.com/hundred666/GoTest/service"
)

type AImpl struct {
	b service.B
}

func (a *AImpl) Foo(s string) (string) {
	return a.b.Add(s)
}

func (a *AImpl) Minus(s string) (string) {
	return strings.Trim(s, "\t")
}

B的设计如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package B

import "github.com/hundred666/GoTest/service"

type BImpl struct {
	a service.A
}

func (b *BImpl) Goo(a string) (string) {
	return b.a.Minus(a)
}

func (b *BImpl) Add(a string) (string) {
	return a + "----"
}

实现了方法,得能够将实例化的变量分别放入A,B结构体中,因此A需要实现以下方法

1
2
3
4
5
6
func NewA() *AImpl {
	return new(AImpl)
}
func (a *AImpl) SetB(b service.B) {
	a.b = b
}

B需要实现以下方法

1
2
3
4
5
6
7
func NewB() *BImpl {
	return new(BImpl)
}

func (b *BImpl) SetA(a service.A) {
	b.a = a
}

这样就完成了整个的设计,需要调用的时候可以这样调用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import (
	"github.com/hundred666/GoTest/B"
	"github.com/hundred666/GoTest/A"
	"fmt"
)

func main() {
	b := B.NewB()
	a := A.NewA()
	a.SetB(b)
	r := a.Foo("aa")
	fmt.Println(r)
}

好熟悉的java的味道!

结论

项目最好是不要出现循环依赖的,出现了大部分情况都是由于项目结构设计不合理导致的,因此,最好在设计项目的时候就考虑好每一层的功能。

参考文献 1. golang解决依赖循环问题... 2. golang不允许循环import问题... 3. 如何解耦循环依赖 4. golang包循环依赖问题... 5. golang package循环依赖的问题