golang之循环导包
作为一个 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之循环导包的更多相关文章
- Golang的循环结构-for语句
Golang的循环结构-for语句 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.for循环语法 循环结构在生活中的场景也非常的多,比如: ()上班族们每天朝九晚五的生活; ( ...
- Android导包导致java.lang.NoClassDefFoundError
摘要: SDK方法总数是不能超过65k的.是否也引入其他的三方库,导致总数超过限制.超出限制会导致部分class找不到,引发java.lang.NoClassDefFoundError.解决方法:近日 ...
- android studio自动导包
http://blog.csdn.net/buaaroid/article/details/44979629 关于导包的设置以上博文解释的很清楚,在此主要强调下这一句: Add unambiguous ...
- Idea设置自动导包
默认 IntelliJ IDEA 是没有开启自动 import 包的功能. 勾选标注 1 选项,IntelliJ IDEA 将在我们书写代码的时候自动帮我们优化导入的包,比如自动去掉一些没有用到的包 ...
- Android studio之更改快捷键及自动导包
更改AS中的代码提示快捷键,AS做的也挺智能的,在Keymap中可以选择使用eclipse的快捷键设置,但是虽然设置了,对有些快捷键还是不能使用,那么就需要我们手动去修改了. 在代码提示AS默认的快捷 ...
- Android Studio没有导包快捷键怎么办
Android Studio没有导包快捷键,那怎么办呢? 在使用Eclipse开发Android应用时,开发者往往会使用Shift+Ctrl+O快捷键来快速导入所有的包,和移除未使用的包.但这个快捷键 ...
- Eclipse之JSON导包
1.选中要导包的工程-–>2.右击选择创建文件夹--->3.将要导的包复制到该文件夹下--–>4.右击要导入的包-->5.选择Build path->Add to Bui ...
- Eclipse Oxygen 解决 自动导包的问题
换成了 Eclipse 的Oxygen 版本 , 发现之前好用的自动导包功能不能用了 (Ctrl+Shift+O) 再 网上看资料 上面说 将 In Windows 替换为Editing Java ...
- java使用*导包的性能
项目中切换到IDEA工具,使用Git提交代码之后在comments中被吐槽了.事情是这样的原有的导入包被IDEA优化了,譬如java.util.Set, java.util.Map, ... 会被优化 ...
- IDEA的导包优化问题
一.现象 文件初始导包状态 package co.x.dw.function; import java.text.SimpleDateFormat; import java.util.ArrayLis ...
随机推荐
- vue单元测试
0.测试钩子函数 describe的钩子函数 在测试块describe中,存在这四个钩子函数,他会在describe执行的不同时期调用: before():在该区块的所有测试用例之前执行 after( ...
- Spring —— bean配置
基础配置 别名配置 作用范围配置 适合交给容器进行管理的bean (复用性的对象,无需重复创建的对象) 表现层对象 业务层对象 数据层对象 工具对象 不适合交给容器管 ...
- Atcoder Beginner Contest 367
A.Shout Everyday \(\text{Diff }43\) 给你 \(24\) 小时制下的 \(A,B,C\) 三个时刻,问 \(A\) 是否在 \([B,C]\) 范围内 考虑到先将 \ ...
- HDK Include Header File (1.7)
Download 1.7 | 1.7.1 | 1.7.2 1.7.1 使用方法:编译选项->目录->C++包含文件->添加 [解压目录]\include 1.7.2 使用方法:编译选 ...
- 暑假集训CSP提高模拟7
这个 T1 的 \(n^{3}\) 的 SPJ 效率还是太慢了,膜拜 SPJ 大神学长,还会画画 A.Permutations & Primes 这题感觉挺水的但是感觉有不是那么水,主要还是因 ...
- CSP-S 2023 游记
CSP-S 2023 游记 Day 0 明天便是 CSP-S 第一轮了,考试前一天万万不能学什么太复杂,太深奥的东西,最好甚至不要过于强度的用脑,保持放空的轻松地状态,心中不要有压力才是最好的考前状态 ...
- 如何关闭每次打开启动软件前的弹窗(用户账户控制)你要允许此应用.WIN11、10、7
1.先点击任务栏内的搜索,输入"控制面板",然后点开 2.然后在右上角输入"更改用户",然后在下方点击"更改用户账户控制设置" 3.然后把& ...
- CentOS 7.9安装ElasticSearch7.14.0、ElasticSearch-Head、Kibana、Node14.18.2
CentOS 7.9安装ElasticSearch7.14.0.ElasticSearch-Head.Kibana.Node14.18.2 1.安装文件 1. elasticsearch-7.14 ...
- string的find()与npos
在 C++ 中,std::string::find() 是一个用于在字符串中查找子字符串或字符的成员函数.查找成功时返回匹配的索引位置,查找失败时返回 std::string::npos,表示未找到. ...
- Android 12 关机重启流程
1. 关机流程 Android上层触发关机的入口很多,但最终几乎都是调用ShutdownThread.shutdown来实现.如下是一些常见的调用关机的点: StatusBarManagerServi ...