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 ...
随机推荐
- 开源项目管理工具 Plane 安装和使用教程
说到项目管理工具,很多人脑海中第一个蹦出来的可能就是 Jira 了.没错,Jira 确实很强大,但是...它也有点太强大了,既复杂又昂贵,而且目前也不再提供私有化部署版本了. 再说说飞书,作为国产之光 ...
- CSS – RWD (Responsive Web Design) 概念篇
介绍 Only PC 以前是没有手机的, 只有电脑, 所以做开发, 只需要开发电脑版本就可以了. Mobile Version 后来手机诞生, 有钱的公司就做两个版本, 一个手机版, 一个电脑版. 没 ...
- CSS – Selector
前言 这篇记入一些我常用到. 以前写的笔记 css 选择器 (学习笔记) Whatever (*) * {} By Id (#) #id {} By Class (.) .class-name {} ...
- Open Graph protocol
大纲介绍 Open Graph protocol 是 facebook 2010 年推出的一套规范, 用途是通过一些简单的 meta tag 来描述网站的内容. 这样在 sharing 网址的时候就可 ...
- SpringBoot——简介&&入门
SpringBoot 简介 SpringBoot是由Pivotal团队提供的全新框架,其设计目的是用来简化Spring应用的初始搭建以及开发过程 起步依赖 starter:SpringBoot中常见的 ...
- [Tkey] OSU!
更新的题解可看 此处 你说得对但是 恐怖日本病毒会自动向你的电脑中下载 OSU! 题意简述 一个 01 串,每个位置有 \(p_{i}\) 的概率为 \(1\),连续的 \(x\) 个 \(1\) 贡 ...
- 智慧医院IT运维方案,全局网络态势感知
随着医疗卫生体制改革不断深化,卫生行业信息化应用不断普及,大数据.AI.医疗物联网等技术的应用,快速推动"智慧医院"建设.以HIS(医院信息系统).EMRS(电子病历系统).PAC ...
- const` 关键字位于函数签名的末尾
在 C++ 中,const 关键字可以应用于成员函数,表示该函数不会修改对象的成员变量. const 出现在 operator->() 成员函数的末尾,这意味着该成员函数在调用时不会修改对象的任 ...
- TX御加固脱壳
示例APP某小说 其实脱这个有好几个方法,我使用了两个方法都可以脱掉. 首先使用Y佬的APK测试: 上传文件后经过等待提示任务成功,把给的ZIP包下载下来. 解压后得到两个文件,txt文件是脱壳后的a ...
- C# 新语法 switch 的简单写法
// C# 中的新语法 switch 的简写 string str = "123"; string res = str switch { "1" => & ...