文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2024-11-30 00:30

关注

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。

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

包循环引用释例

对象导致的包循环引用

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

图片

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

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

如何解决?有两种方案:

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

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

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

图片

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

看来之前还是随意惯了。

图片

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

启示:

internal 引用了 share

一键三连。

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

图片

这个是因为 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 就不那么幸运了(也许是幸运的,因为它让你早点发现问题)。

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

图片

对策汇总

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

(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 语言作者怎么说:

图片

再听听 AI 怎么说 :

小结

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

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

参考资料

Reference

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

来源:编程大观园内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯