作为一个 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. (八)Redis 主从复制、切片集群

    一.主从复制 1.主从关系 都说的 Redis 具有高可靠性,这里有两层含义:一是数据尽量少丢失,二是服务尽量少中断.AOF 和 RDB 保证了前者,而对于后者,Redis 的做法就是将一份数据同时保 ...

  2. 使用 nuxi prepare 命令准备 Nuxt 项目

    title: 使用 nuxi prepare 命令准备 Nuxt 项目 date: 2024/9/7 updated: 2024/9/7 author: cmdragon excerpt: 摘要:本文 ...

  3. 爬虫案例2-爬取视频的三种方式之一:selenium篇(2)

    @ 目录 前言 selenium简介 实战案例 共勉 博客 前言 继使用requests库爬取好看视频的文章后,本文分享使用python第三方库selenium库接着来爬取视频网站,后续也会接着分享使 ...

  4. DLA:动态层级注意力架构,实现特征图的持续动态刷新与交互 | IJCAI'24

    论文深入探讨了层级注意力与一般注意力机制之间的区别,并指出现有的层级注意力方法是在静态特征图上实现层间交互的.这些静态层级注意力方法限制了层间上下文特征提取的能力.为了恢复注意力机制的动态上下文表示能 ...

  5. 我的网站集成ElasticSearch初体验

    最近,我给我的网站(https://www.xiandanplay.com/)尝试集成了一下es来实现我的一个搜索功能,因为这个是我第一次了解运用elastic,所以如果有不对的地方,大家可以指出来, ...

  6. I found that CTH has no RP when i tried to reduce his RP

  7. [TK] 一心净士 hzoj-tg-937-2

    万元申万的(不是) 嗯... 另外,这道题其实叫一心净士(shi) 而不是一心净土. 剖析 我们注意到题目要让我们使最小的自然数最大,那么我们的每一个区间都要从零开始放. 显然,假如我们所有区间里最小 ...

  8. [Tkey] Transport Nekomusume II

    CL-20 考虑定义一条有向边 \(u\rightarrow v\) 的意义为 \(u\) 把窝让给了 \(v\),那么每个点一定入度为 \(1\),所有的边会形成一个外向基环树森林. 贪心地把猫娘按 ...

  9. SimpleRAG-v1.0.3:增加文件对话功能

    Kimi上有一个功能,就是增加文件之后对话,比如我有如下一个私有文档: 会议主题:<如何使用C#提升工作效率> 参会人员:张三.李四.王五 时间:2024.9.26 14:00-16:00 ...

  10. 浅谈数栈产品里的 Descriptions 组件

    我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品.我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值. 本文作者:修能 What's? 数栈产品里的 Description ...