Sivo 悉见

解决golang循环依赖

匿名 · 更新于 2023/7/24

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

为了方便展示,我将整个项目抽象了下,项目结果如下 
 
项目的错误如下 

 


包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循环依赖的问题