脚本之家,脚本语言编程技术及教程分享平台!
分类导航

Python|VBS|Ruby|Lua|perl|VBA|Golang|PowerShell|Erlang|autoit|Dos|bat|shell|

服务器之家 - 脚本之家 - Golang - Go 包循环引用及对策,你学会了吗?

Go 包循环引用及对策,你学会了吗?

2024-03-18 16:09编程大观园 Golang

在 Java 里面,循环依赖是类级别的;但 Go 里要更严格一些:Go 的循环引用判定是 包级别的。举个例子,包 A 下的类 A 依赖了包 B 下的类 B,类 B 又依赖了包 C 下的类 C, 类 C 又依赖了包 A 下的 D。

引言

从 Java 转到 Go 的开发同学,大概都会踩到第一个“坑”:Go 的包循环引用。

Go 的包循环引用是什么意思呢?有一定经验的开发者都知道循环依赖,比如 A 依赖了 B, B 依赖了 C ,C 又依赖了 A。这就构成了一个循环依赖(有环图)。

在 Java 里面,循环依赖是类级别的;但 Go 里要更严格一些:Go 的循环引用判定是 包级别的。举个例子,包 A 下的类 A 依赖了包 B 下的类 B,类 B 又依赖了包 C 下的类 C, 类 C 又依赖了包 A 下的 D。在 Java 里面,这里并没有构成循环依赖。但在 Go 里面,这导致了包循环引用:包 A => 包 B = > 包 C => 包 A。Go 会编译不通过:报 import circle not allowed。

对包依赖不太重视的人,初期会感到不适应。本文举几个自己踩过的坑,作一说明。

包循环引用释例

对象导致的包循环引用

哎呀呀,初来乍到,一下子给我来了八个包循环引用,打击得我有点不知所措了。这是怎么回事呢 ?

Go 包循环引用及对策,你学会了吗?图片

第一个循环引用,是因为上报的包 agent 下对象 DetectionBase 依赖了包 denoise 下的降噪模型结果对象 HitDenoiseModel,而在某一处,HitDenoiseModel  又引用了包 agent 下的另一个对象。

为什么会发生这个事情?这是因为,图省事,我直接把输出对象放到了输入对象的包里,不想复制一份。正确的做法是,输入的对象只能放输入对象,不能放输出对象,否则依赖就会扩大出去。

如何解决?有两种方案:

  • 把 HitDenoiseModel 放在 agent 包下。然后定义另一个对象 denoise/DenoiseResultModel,将 FillHitDenoiseModel 方法移到包 denoise 下,HitDenoiseModel 赋值给 DenoiseResultModel。这样形成了包 denoise => agent 的单向依赖,保证“输出 =>输入” 的单向依赖。不过,这种方法还是存在“篡改”原始数据的小罪行。
  • 不对 DetectionBase 做任何变更,新创建一个包和对象,把 HitDenoiseModel 放在新创建对象里,同时将 FillHitDenoiseModel 方法移到新创建对象的包下。3

启示:避免篡改输入对象。应用中的对象往往是很多的,不太重视包依赖,很容易造成对象的循环引用。即使你自己能够小心确保不出问题,也会和别人添加的类造成循环引用。

发送消息与接收消息循环引用

梅开二度。又来了一个包循环引用。

Go 包循环引用及对策,你学会了吗?图片

这又是怎么回事?有了第一次经验,第二次就不那么慌了。先提个 MR ,看看引入了哪些类。尤其是循环引用的那条链路。我们看到 cdc/msg 这个地方作为起点开始循环。为什么会有循环呢?因为我本来打算把 msg 相关的消息对象和消息处理都放在一起。但这种方式很容易导致 循环引用。为什么呢?因为 引入消息对象的时候, 就会把包下的所有类引入的所有包都引进来,这样就会把 /cdc/msg/XXXReceiver 引入,进而引入 /cdc/handler/XXXhandler, 而 msg/handler/XXXhandler 又会引入 msg/XXXSender。就导致了包循环引用依赖。

看来之前还是随意惯了。

Go 包循环引用及对策,你学会了吗?图片

怎么解决?把 XXXReceiver 放在包 receiver 下,把 XXXSender 放在包 sender 下即可。

启示:

  • 引入类的包要小,这样就很类似 Java 的全限定性包,减少循环引用的可能性。
  • 简单的消息对象与复杂的消息处理不要放在一起,因为引入简单的就会把复杂的引入,复杂的又会递归引入更多的依赖。

internal 引用了 share

一键三连。

下图又是怎么回事?借鉴业界最佳实践,咱们把一个工程下的代码分为了 internal 和 share。其中 internal 的代码不能被其它模块访问,只有 share 下的代码作为桥梁,为其它模块提供服务。

Go 包循环引用及对策,你学会了吗?图片

这个是因为 internal/detect_config/AService 引用了 share/detect_config/BService ,然后 share/detect_config/CService 又引用了 internal/detect_config/DService。internal 包怎么能够引用 share 下的包呢 ?内部模块怎么能够依赖外部模块 ?世界似乎不那么美好了。

与同事讨论,他们认为,helper 不应该依赖 service ,而应该依赖 repository, 而我一直认为 helper 是对 service 提供服务的一种高层封装, helper 依赖 service 是很正常。helper 依赖 repository, 看上去说得也很有道理。不过 internal 模块依赖 share 模块,看上去总是感觉有点违反单向依赖的设计原则。

经过讨论后,我和同事各做了修改。我的修改是让 helper 依赖 repository, 同事的修改是,把原来 service 拆分成公共的 service 和内部的 service,保证 share 对 internal 的单向依赖。

运行时循环依赖

即使你幸运地逃过了包循环引用的检测,但存在运行时循环依赖,Go 会直接卡住。

一个例子如下图所示:有若干个流程组件 A, B, C,加载到一个工厂 F 下,由一个 执行器 E 依次从 F 中取出执行。但是呢,在流程组件C 中,又依赖了 E 来执行任务。这样会导致循环依赖。在 Java 中,Spring 会忽略组件 C, 初始化成功,但运行时找不到 C 而导致流程出错(可以用懒加载机制解决);但是 Go 就不那么幸运了(也许是幸运的,因为它让你早点发现问题)。

对于这种场景,可以用消息队列来解耦。

Go 包循环引用及对策,你学会了吗?图片

对策汇总

遇到包循环引用,有哪些经验可循呢 ?

(1) 提倡小而独立的包。不要把大量的有关联的类都放在一个包下。这样,很容易因为一个类的引入,而引入更多依赖,导致依赖不可控。

(2)单向依赖原则。internal 下的类不应当依赖 share 下的类。因为 share 下的类一定会依赖 internal。如果 internal 又依赖 share ,就破坏了“单向依赖”原则。当工程越来越大时,一定会有一个点会爆发。不是不报,时候未到。细分包可以缓解这种问题,但不能从根本上避免单向依赖的破坏。

(3) 依赖倒置原则。尽量依赖接口,而不是具体实现类。

(4)使用消息队列解耦依赖。

(5)相对合理的依赖方向:model => (constants, 无依赖的 model) ; dto => types => constants ;util => (dto, types, constants, models);service =>  repository => model ;helper => (repository, util, cache) ; controller => (helper, service) ;  receiver => handler => (service, helper)

最后问一句:Go 为什么不允许包循环引用呢 ?听听 Go 语言作者怎么说:

Go 包循环引用及对策,你学会了吗?图片

再听听 AI 怎么说 :

Go 包循环引用及对策,你学会了吗?

小结

本文讨论了几个包循环引用的例子,并给出了相应的对策。

个人觉得,这种禁止循环引用的做法还是可取的,能培养良好的设计习惯。软件开发,本质是应对结构复杂性的技艺。而设计思维,则是应对结构复杂性的重要法宝。开发人员应多多学习设计思考,保持系统和架构的优雅。

参考资料

  • go循环依赖最佳解决方案[1]

Reference

[1]go循环依赖最佳解决方案:https://juejin.cn/post/7290389972406501432

原文地址:https://mp.weixin.qq.com/s/l__2oXSsmyqqw2KKneS1aQ

延伸 · 阅读

精彩推荐
  • GolangGo slice切片make生成append追加copy复制示例

    Go slice切片make生成append追加copy复制示例

    这篇文章主要为大家介绍了Go使用make生成切片、使用append追加切片元素、使用copy复制切片使用示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助...

    王中阳Go7552022-10-19
  • Golang在Golang中简化日志记录:提升性能和调试效率

    在Golang中简化日志记录:提升性能和调试效率

    Golang中有效的日志记录实践超越了简单的错误跟踪;它们是应用程序弹性和性能优化的基石。通过拥抱结构化日志、优化性能,并与监控工具集成,开发人...

    技术的游戏8832024-02-28
  • Golanggo等待一组协程结束的操作方式

    go等待一组协程结束的操作方式

    这篇文章主要介绍了go等待一组协程结束的操作方式,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    cj_2866302021-06-15
  • GolangGo easyjson使用及反射原理

    Go easyjson使用及反射原理

    这篇文章主要介绍了Go easyjson使用技巧,详细介绍了go自带JSON库使用的反射原理,性能相对较差,可以使用easyjson代替,需要的朋友可以参考下...

    周伯通10282022-09-24
  • Golangjenkins配置golang 代码工程自动发布的实现方法

    jenkins配置golang 代码工程自动发布的实现方法

    这篇文章主要介绍了jenkins配置golang 代码工程自动发布,jks是个很好的工具,使用方法也很多,我只用了它简单的功能,对jenkins配置golang相关知识感兴趣的朋...

    stefan124011762022-07-26
  • GolangGo Gin框架请求自动验证和数据绑定

    Go Gin框架请求自动验证和数据绑定

    今天把使用 Gin 框架开发项目时,经常会用到的请求数据的模型绑定和验证统一梳理了一下,基本上没什么废话都是代码。除了模型绑定和验证,我们还把...

    网管叨bi叨8752022-10-18
  • GolangGolang 使用接口实现泛型的方法示例

    Golang 使用接口实现泛型的方法示例

    这篇文章主要介绍了Golang 使用接口实现泛型的方法示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友...

    Ovenvan4902020-05-23
  • Golang以alpine作为基础镜像构建Golang可执行程序操作

    以alpine作为基础镜像构建Golang可执行程序操作

    这篇文章主要介绍了以alpine作为基础镜像构建Golang可执行程序操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    思维的深度10092021-03-14