轉載至:https://studygolang.com/articles/9072
無論何種語言,依賴管理都是一個比較複雜的問題。而Go語言中的依賴管理機制目前還是讓人比較失望的。在1.6版本之前,官方只有把依賴放在GOPATH中,並沒有多版本管理機制;1.6版本(1.5版本是experimental feature)引入vendor機制,是包依賴管理對一次重要嘗試。他在Go生態系統中依然是一個熱門的爭論話題,還沒有想到完美的解決方案。
看其它
我們先來看看其它語言怎麼解決,例舉兩種典型的管理方式:
Java
開發態,可以通過maven和gradle工具編輯依賴清單列表/指令碼,指定依賴庫的位置/版本等資訊,這些可以協助你在合適的時間將項目固化到一個可隨時隨地重複編譯發布的狀態。這些工具對我來說已經足夠優雅有效。但maven中也有不同依賴庫的內部依賴版本衝突等令人心煩的問題。尤其是在大型項目中的依賴傳遞問題,若團隊成員對maven機制沒有足夠瞭解下,依賴scope的濫用,會讓整個項目工程的依賴樹變得特別的巨大而每次編譯效率低下。運行態,目前Java也沒有很好的依賴管理機制,雖有classloader可以做一定的隔離,但像OSGi那種嚴格的版本管理,會讓使用者陷入多版本相互衝突的泥潭。
Node.js
npm是Node.js的首選模組依賴管理工具。npm通過一個目前的目錄的 package.json 檔案來描述模組的依賴,在這個檔案裡你可以定義你的應用程式名稱( name )、應用描述( description )、關鍵字( keywords )、版本號碼( version )等。npm會下載當前項目相依模組到你項目中的一個叫做node_modules的檔案夾內。與maven/gradle不同的是,maven最終會分析依賴樹,把相同的軟體預設扁平化取最高版本。而npm支援nested dependency tree。nested dependency tree是每個模組依賴自己目錄下node_modules中的模組,這樣能避免了依賴衝突, 但耗費了更多的空間和時間。由於Javascript是源碼發布,所以開發態與運行態的依賴都是基於npm,優先從自己的node_modules搜尋依賴的模組。
go get
Go對包管理一定有自己的理解。對於包的擷取,就是用go get命令從遠程程式碼程式庫(GitHub, Bitbucket, Google Code, Launchpad)拉取。這樣做的好處是,直接跳過了包管理中央庫的的約束,讓代碼的拉取直接基於版本控制庫,大家的協作管理都是基於這個版本依賴庫來互動。細體會下,發現這種設計的好處是去掉冗餘,直接複用最基本的代碼基礎設施。Golang這麼幹很大程度上減輕了開發人員對包管理的複雜概念的理解負擔,設計的很巧妙。
當然,go get命令,仍然過於簡單。對於現實過程中的開發人員來說,仍然有其痛苦的地方:
- 缺乏明確顯示的版本。團隊開發不同的項目容易匯入不一樣的版本,每次都是get最新的代碼。尤其像我司對開源軟體管理非常嚴格,開源申請幾乎是無法實施。
- 第三方包沒有Alibaba Content Security Service審計,擷取最新的代碼很容易引入代碼新的Bug,後續運行時出了Bug需要解決,也無法版本跟蹤管理。
- 依賴的完整性無法校正,基於網域名稱的package名稱,網域名稱變化或子路徑變化,都會導致無法正常下載依賴。我們在使用過程,發現還是有不少間接依賴包的名稱已失效了(不存在,或又fork成新的項目,舊的已不存維護更新)。
而Go官方對於此類問題的建議是把外部依賴的代碼複製到你的 源碼庫中管理 。把第三方代碼引入自己的程式碼程式庫仍然是一種折中的辦法,對於像我司的軟體開發流程來說,是不現實的:
- 開源掃描會掃描出是相似的代碼時,若License不是寬鬆的,則涉及到法律風險,若是寬鬆的,開源掃描認證確認工作也很繁瑣。
- 如何升級版本,代碼複製過來之後,源始的項目的代碼可以變化很大了,無明顯的版本校正,藉助工具或指令碼來升級也會帶來工作量很大。
- 複製的那一份代碼已經開始變成私人,第三方代碼的Bug只能自己解決,難以貢獻代碼來修複Bug,或通過推動社區來解決。
- 普通的程式問題可能不是很大問題,最多就是編譯時間的依賴。但如果你寫的是一個給其他人使用的lib庫,引入這個庫就會帶來麻煩了。你這個庫被多人引用,如何管理你這個庫的代碼依賴呢?
好在開源的力量就是大,Go官方沒有想清楚的版本管理問題,社區就會有人來解決,我們已經可以找到許多不錯的解決方案,不妨先參考下 官方建議 。
vendor機制
vendor是1.5引入為體驗,1.6中正式發布的依賴管理特性。Go團隊在推出vendor前已經在Golang-dev group上做了長時間的調研。最終Russ Cox在 Keith Rarick 的proposal的基礎上做了改良,形成了Go 1.5中的vendor:
- 不rewrite gopath
- go tool來解決
- go get相容
- 可reproduce building process
並給出了vendor機制的”4行”詮釋:
If there is a source directory d/vendor, then, when compiling a source file within the subtree rooted at d, import “p” is interpreted as import “d/vendor/p” if that exists.
When there are multiple possible resolutions,the most specific (longest) path wins.
The short form must always be used: no import path can contain “/vendor/” explicitly.
Import comments are ignored in vendored packages.
總結解釋起來:
- vendor是一個特殊的目錄,在應用的源碼目錄下,go doc工具會忽略它。
- vendor機制支援嵌套vendor,vendor中的第三方包中也可以包含vendor目錄。
- 若不同層次的vendor下存在相同的package,編譯尋找路徑優先搜尋當前pakcage下的vendor是否存在,若沒有再向parent pacakge下的vendor搜尋(x/y/z作為parentpath輸入,搜尋路徑:x/y/z/vendor/path->x/y/vendor/path->x/vendor/path->vendor/path)
- 在使用時不用理會vendor這個路徑的存在,該怎麼import包就怎麼import,不要出現import “d/vendor/p”的情況。vendor是由go tool隱式處理的。
- 不會校正vendor中package的import path是否與canonical import路徑是否一致了。
vendor機制看似像node.js的node_modules,支援嵌套vendor,若一個工程中在著兩個版本的相的包,可以放在不同的層次的vendor下:
- 優點:可能解決不同的版本依賴衝突問題,不同的層次的vendor存放在不同的vendor。
- 缺點:由於go的package是以路徑組織的,在編譯時間,不同層次的vendor中相同的包會編譯兩次,連結兩份,程式檔案變大,運行期是執行不同的代碼邏輯。會導致一些問題,如果在package init中全域初始化,可能重複初化出問題,也可能初化為不同的變數(記憶體中不同),無法共用擷取。像之前我們遇到gprc類似的問題就是不同層次的相同package重複init導致的,見 社區反饋 。
所以Russ Cox期望大家良好設計工程布局,作為lib的包 不攜帶vendor更佳 ,一個project內的所有vendor都集中在頂層vendor裡面。