Go 包循環(huán)引用及對策,你學(xué)會了嗎?
引言
從 Java 轉(zhuǎn)到 Go 的開發(fā)同學(xué),大概都會踩到第一個(gè)“坑”:Go 的包循環(huán)引用。
Go 的包循環(huán)引用是什么意思呢?有一定經(jīng)驗(yàn)的開發(fā)者都知道循環(huán)依賴,比如 A 依賴了 B, B 依賴了 C ,C 又依賴了 A。這就構(gòu)成了一個(gè)循環(huán)依賴(有環(huán)圖)。
在 Java 里面,循環(huán)依賴是類級別的;但 Go 里要更嚴(yán)格一些:Go 的循環(huán)引用判定是 包級別的。舉個(gè)例子,包 A 下的類 A 依賴了包 B 下的類 B,類 B 又依賴了包 C 下的類 C, 類 C 又依賴了包 A 下的 D。在 Java 里面,這里并沒有構(gòu)成循環(huán)依賴。但在 Go 里面,這導(dǎo)致了包循環(huán)引用:包 A => 包 B = > 包 C => 包 A。Go 會編譯不通過:報(bào) import circle not allowed。
對包依賴不太重視的人,初期會感到不適應(yīng)。本文舉幾個(gè)自己踩過的坑,作一說明。
包循環(huán)引用釋例
對象導(dǎo)致的包循環(huán)引用
哎呀呀,初來乍到,一下子給我來了八個(gè)包循環(huán)引用,打擊得我有點(diǎn)不知所措了。這是怎么回事呢 ?
圖片
第一個(gè)循環(huán)引用,是因?yàn)樯蠄?bào)的包 agent 下對象 DetectionBase 依賴了包 denoise 下的降噪模型結(jié)果對象 HitDenoiseModel,而在某一處,HitDenoiseModel 又引用了包 agent 下的另一個(gè)對象。
為什么會發(fā)生這個(gè)事情?這是因?yàn)椋瑘D省事,我直接把輸出對象放到了輸入對象的包里,不想復(fù)制一份。正確的做法是,輸入的對象只能放輸入對象,不能放輸出對象,否則依賴就會擴(kuò)大出去。
如何解決?有兩種方案:
- 把 HitDenoiseModel 放在 agent 包下。然后定義另一個(gè)對象 denoise/DenoiseResultModel,將 FillHitDenoiseModel 方法移到包 denoise 下,HitDenoiseModel 賦值給 DenoiseResultModel。這樣形成了包 denoise => agent 的單向依賴,保證“輸出 =>輸入” 的單向依賴。不過,這種方法還是存在“篡改”原始數(shù)據(jù)的小罪行。
- 不對 DetectionBase 做任何變更,新創(chuàng)建一個(gè)包和對象,把 HitDenoiseModel 放在新創(chuàng)建對象里,同時(shí)將 FillHitDenoiseModel 方法移到新創(chuàng)建對象的包下。3
啟示:避免篡改輸入對象。應(yīng)用中的對象往往是很多的,不太重視包依賴,很容易造成對象的循環(huán)引用。即使你自己能夠小心確保不出問題,也會和別人添加的類造成循環(huán)引用。
發(fā)送消息與接收消息循環(huán)引用
梅開二度。又來了一個(gè)包循環(huán)引用。
圖片
這又是怎么回事?有了第一次經(jīng)驗(yàn),第二次就不那么慌了。先提個(gè) MR ,看看引入了哪些類。尤其是循環(huán)引用的那條鏈路。我們看到 cdc/msg 這個(gè)地方作為起點(diǎn)開始循環(huán)。為什么會有循環(huán)呢?因?yàn)槲冶緛泶蛩惆?msg 相關(guān)的消息對象和消息處理都放在一起。但這種方式很容易導(dǎo)致 循環(huán)引用。為什么呢?因?yàn)?引入消息對象的時(shí)候, 就會把包下的所有類引入的所有包都引進(jìn)來,這樣就會把 /cdc/msg/XXXReceiver 引入,進(jìn)而引入 /cdc/handler/XXXhandler, 而 msg/handler/XXXhandler 又會引入 msg/XXXSender。就導(dǎo)致了包循環(huán)引用依賴。
看來之前還是隨意慣了。
圖片
怎么解決?把 XXXReceiver 放在包 receiver 下,把 XXXSender 放在包 sender 下即可。
啟示:
- 引入類的包要小,這樣就很類似 Java 的全限定性包,減少循環(huán)引用的可能性。
- 簡單的消息對象與復(fù)雜的消息處理不要放在一起,因?yàn)橐牒唵蔚木蜁褟?fù)雜的引入,復(fù)雜的又會遞歸引入更多的依賴。
internal 引用了 share
一鍵三連。
下圖又是怎么回事?借鑒業(yè)界最佳實(shí)踐,咱們把一個(gè)工程下的代碼分為了 internal 和 share。其中 internal 的代碼不能被其它模塊訪問,只有 share 下的代碼作為橋梁,為其它模塊提供服務(wù)。
圖片
這個(gè)是因?yàn)?internal/detect_config/AService 引用了 share/detect_config/BService ,然后 share/detect_config/CService 又引用了 internal/detect_config/DService。internal 包怎么能夠引用 share 下的包呢 ?內(nèi)部模塊怎么能夠依賴外部模塊 ?世界似乎不那么美好了。
與同事討論,他們認(rèn)為,helper 不應(yīng)該依賴 service ,而應(yīng)該依賴 repository, 而我一直認(rèn)為 helper 是對 service 提供服務(wù)的一種高層封裝, helper 依賴 service 是很正常。helper 依賴 repository, 看上去說得也很有道理。不過 internal 模塊依賴 share 模塊,看上去總是感覺有點(diǎn)違反單向依賴的設(shè)計(jì)原則。
經(jīng)過討論后,我和同事各做了修改。我的修改是讓 helper 依賴 repository, 同事的修改是,把原來 service 拆分成公共的 service 和內(nèi)部的 service,保證 share 對 internal 的單向依賴。
運(yùn)行時(shí)循環(huán)依賴
即使你幸運(yùn)地逃過了包循環(huán)引用的檢測,但存在運(yùn)行時(shí)循環(huán)依賴,Go 會直接卡住。
一個(gè)例子如下圖所示:有若干個(gè)流程組件 A, B, C,加載到一個(gè)工廠 F 下,由一個(gè) 執(zhí)行器 E 依次從 F 中取出執(zhí)行。但是呢,在流程組件C 中,又依賴了 E 來執(zhí)行任務(wù)。這樣會導(dǎo)致循環(huán)依賴。在 Java 中,Spring 會忽略組件 C, 初始化成功,但運(yùn)行時(shí)找不到 C 而導(dǎo)致流程出錯(cuò)(可以用懶加載機(jī)制解決);但是 Go 就不那么幸運(yùn)了(也許是幸運(yùn)的,因?yàn)樗屇阍琰c(diǎn)發(fā)現(xiàn)問題)。
對于這種場景,可以用消息隊(duì)列來解耦。
圖片
對策匯總
遇到包循環(huán)引用,有哪些經(jīng)驗(yàn)可循呢 ?
(1) 提倡小而獨(dú)立的包。不要把大量的有關(guān)聯(lián)的類都放在一個(gè)包下。這樣,很容易因?yàn)橐粋€(gè)類的引入,而引入更多依賴,導(dǎo)致依賴不可控。
(2)單向依賴原則。internal 下的類不應(yīng)當(dāng)依賴 share 下的類。因?yàn)?share 下的類一定會依賴 internal。如果 internal 又依賴 share ,就破壞了“單向依賴”原則。當(dāng)工程越來越大時(shí),一定會有一個(gè)點(diǎn)會爆發(fā)。不是不報(bào),時(shí)候未到。細(xì)分包可以緩解這種問題,但不能從根本上避免單向依賴的破壞。
(3) 依賴倒置原則。盡量依賴接口,而不是具體實(shí)現(xiàn)類。
(4)使用消息隊(duì)列解耦依賴。
(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 為什么不允許包循環(huán)引用呢 ?聽聽 Go 語言作者怎么說:
圖片
再聽聽 AI 怎么說 :
小結(jié)
本文討論了幾個(gè)包循環(huán)引用的例子,并給出了相應(yīng)的對策。
個(gè)人覺得,這種禁止循環(huán)引用的做法還是可取的,能培養(yǎng)良好的設(shè)計(jì)習(xí)慣。軟件開發(fā),本質(zhì)是應(yīng)對結(jié)構(gòu)復(fù)雜性的技藝。而設(shè)計(jì)思維,則是應(yīng)對結(jié)構(gòu)復(fù)雜性的重要法寶。開發(fā)人員應(yīng)多多學(xué)習(xí)設(shè)計(jì)思考,保持系統(tǒng)和架構(gòu)的優(yōu)雅。
參考資料
- go循環(huán)依賴最佳解決方案[1]
Reference
[1]go循環(huán)依賴最佳解決方案:https://juejin.cn/post/7290389972406501432