作为一个 Golang 开发,你可能在项目中遇到过包的循环依赖问题。Golang 不允许循环依赖,如果检测到代码中存在这种情况,在编译时就会抛出异常。

循环依赖

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

实例:

项目结构:

import-loop

  p1

    p1.go

  p2

    p2.go

  main.go

其中:

p1.go

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()
}

p2.go

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编译器不会花很多时间去生成最高效的机器码,它更关心的是快速编译大量源码。

支持循环依赖功能会大大增加代码的编译时长,因为每当其中一个依赖发生变化时,整个依赖关系就需要重新编译。其还会增加链接(link)时间,并让独立测试、包重用变得更加困难 (由于包之间不能保证隔离性,单元测试会变得更困难)。循环依赖有时还会导致无限递归。

循环依赖还有可能导致内存泄露,因为一个对象会引用另一个对象,它们的引用计数永远不会变成0,因此永远不会成为收集和清理的对象。

Robe Pike 在:Golang是否会支持循环依赖的提案中答复道:这是一个需要前置简化的领域,循环依赖虽然能带来一定便捷,但其成本是灾难性的。应该被继续禁止。

调试循环依赖

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

但Go语言会在报错信息中告诉你导致问题的package名,因此可以通过包名来解决问题。

也可以使用godepgraph工具, 把项目中包之间的依赖关系可视化,可以通过这个指令进行安装:

go install github.com/kisielk/godepgraph

它会以 Graphviz 点格式展示依赖图。如果你安装了graphviz工具(没有的话可以通过这个链接下载),你可以通过管道命令输出dot格式来渲染依赖图。

graphviz的安装: https://gitlab.com/api/v4/projects/4207231/packages/generic/graphviz-releases/8.1.0/windows_10_cmake_Release_graphviz-install-8.1.0-win64.exe

godepgraph -s import-cycle-example | dot -Tpng -o godepgraph.png

生成的图片:

解决循环依赖问题

当你遇到循环依赖问题时,先思考项目的组织关系是否合理。处理循环依赖最常见的方法是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.go:

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()
}

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

丑陋的解决方式

有趣的是,你可以通go:linkname注释来避免导入包。go:linkname是一个编译器指令(格式://go:linkname localname [importpath.name] ) 。这个特殊指令的作用域不是紧跟的下一行代码,而是在同一个包下生效。//go:linkname 告诉Go的编译器本本地的变量或方法 localname 链接到指令的变量或方法 importpath.name 上go:linkname定义 。听起来可能有点难以理解,可以参考后面的源码,来试着用它来解决循环引用问题。

Go的很多标准包都依赖go:linktime运行时的私有调用。你可以使用它来解决你代码中的循环引用问题,但应该避免使用,因为这是Go官方的黑科技,他们自己也不建议使用。

需要注意的是,Go的标准包使用go:linkname不是为了避免循环依赖,而是用它避免导出不应该公开的API。

下面是使用go:linkname方案解决循环依赖的源码:jogendra/import-cycle-example-go -> golinkname。

结语

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

golang之循环导包的更多相关文章

  1. Golang的循环结构-for语句

    Golang的循环结构-for语句 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.for循环语法 循环结构在生活中的场景也非常的多,比如: ()上班族们每天朝九晚五的生活; ( ...

  2. Android导包导致java.lang.NoClassDefFoundError

    摘要: SDK方法总数是不能超过65k的.是否也引入其他的三方库,导致总数超过限制.超出限制会导致部分class找不到,引发java.lang.NoClassDefFoundError.解决方法:近日 ...

  3. android studio自动导包

    http://blog.csdn.net/buaaroid/article/details/44979629 关于导包的设置以上博文解释的很清楚,在此主要强调下这一句: Add unambiguous ...

  4. Idea设置自动导包

    默认 IntelliJ IDEA 是没有开启自动 import 包的功能.  勾选标注 1 选项,IntelliJ IDEA 将在我们书写代码的时候自动帮我们优化导入的包,比如自动去掉一些没有用到的包 ...

  5. Android studio之更改快捷键及自动导包

    更改AS中的代码提示快捷键,AS做的也挺智能的,在Keymap中可以选择使用eclipse的快捷键设置,但是虽然设置了,对有些快捷键还是不能使用,那么就需要我们手动去修改了. 在代码提示AS默认的快捷 ...

  6. Android Studio没有导包快捷键怎么办

    Android Studio没有导包快捷键,那怎么办呢? 在使用Eclipse开发Android应用时,开发者往往会使用Shift+Ctrl+O快捷键来快速导入所有的包,和移除未使用的包.但这个快捷键 ...

  7. Eclipse之JSON导包

    1.选中要导包的工程-–>2.右击选择创建文件夹--->3.将要导的包复制到该文件夹下--–>4.右击要导入的包-->5.选择Build path->Add to Bui ...

  8. Eclipse Oxygen 解决 自动导包的问题

    换成了 Eclipse 的Oxygen 版本 , 发现之前好用的自动导包功能不能用了 (Ctrl+Shift+O) 再 网上看资料  上面说 将  In Windows 替换为Editing Java ...

  9. java使用*导包的性能

    项目中切换到IDEA工具,使用Git提交代码之后在comments中被吐槽了.事情是这样的原有的导入包被IDEA优化了,譬如java.util.Set, java.util.Map, ... 会被优化 ...

  10. IDEA的导包优化问题

    一.现象 文件初始导包状态 package co.x.dw.function; import java.text.SimpleDateFormat; import java.util.ArrayLis ...

随机推荐

  1. sql server 将数据库表里面的数据,转为insert语句,方便小批量转移数据

    create proc [dbo].[proc_insert] (@tablename varchar(256)) as begin set nocount on declare @sqlstr va ...

  2. 【YashanDB数据库】大事务回滚导致其他操作无法执行,报错YAS-02016 no free undo blocks

    问题现象 客户将一个100G的表的数据插入到另一个表中,使用insert into select插入数据.从第一天下午2点开始执行,到第二天上午10点,一直未执行完毕. 由于需要实施下一步操作,客户k ...

  3. C语言实现一个走迷宫小游戏(深度优先算法)

    补充一下,先前文章末尾给出的下载链接的完整代码含有部分C++的语法(使用Dev-C++并且文件扩展名为.cpp的没有影响),如果有的朋友使用的语言标准是VC6的话可能不支持,所以在修改过后再上传一版, ...

  4. 个人Blog的第一篇博文

    个人Blog的第一篇博文 正式加入"博客园"大家庭了,希望以后可以一直坚持下去欸.

  5. 15. 序列化模块json和pickle、os模块

    1. 序列化模块 1.1 序列化与反序列化 (1)序列化 将原本的python数据类型字典.列表.元组 转换成json格式字符串的过程就叫序列化 (2)反序列化 将json格式字符串转换成python ...

  6. excel江湖异闻录--修迪斯.嗦狸

    因为技术出类拔萃,同学都尊称他为"修神",修神的python.vba.Javascript.java.数据库.批处理等众多编程语言都是极强的,以笔者的见识来判断,大佬的vba已经是 ...

  7. Java日期时间API系列14-----Jdk8中java.time包中的新的日期时间API类,java日期计算1,获取年月日时分秒等

    通过Java日期时间API系列8-----Jdk8中java.time包中的新的日期时间API类的LocalDate源码分析 ,可以看出java8设计非常好,实现接口Temporal, Tempora ...

  8. 1. react项目【前端】+C#【后端】从0到1

    1.创建前端基础框架 1.1 前端创建 软件: 1.1.1 npx create-react-app pc ps:pc 是文件名 : 1.1.2 npm start 启动项目 2.创建后端基础框架 软 ...

  9. .NET 内存管理两种有效的资源释放方式

    前言 嗨,大家好!今天我们要聊一聊 .NET 中的内存管理.你知道吗?虽然 .NET 有一个很好的垃圾回收系统来自动清理不再使用的对象,但在某些情况下,我们还需要自己动手来释放一些特殊的资源,比如打开 ...

  10. python之调用高德、百度api解析经纬度地址

    调用高德 # 高德地图根据经纬度反查地址,每天只能调用5000次 def gaode_excute_single_query(coordStrings ,currentkey='你自己的api-key ...