理解Go 1.5 vendor
Go 1.5中(目前最新版本go1.5beta3)加入了一個experimental feature:?vendor/。這個feature不是Go 1.5的正式功能,但卻是Go Authors們在解決Go被外界詬病的包依賴管理的道路上的一次重要嘗試。目前關于Go vendor機制的資料有限,主要的包括如下幾個:
1、Russ Cox在Golang-dev group上的一個名 為"proposal: external packages" topic上的reply。
2、Go 1.5beta版發布后Russ Cox根據上面topic整理的一個doc。
3、medium.com上一篇名為“Go 1.5 vendor/ experiment"的文章。
但由于Go 1.5穩定版還未發布(最新消息是2015.8月中旬發布),因此估計真正采用vendor的repo尚沒有。但既然是Go官方解決方案,后續從 expreimental變成official的可能性就很大(Russ的初步計劃:如果試驗順利,1.6版本默認 GO15VENDOREXPERIMENT="1";1.7中將去掉GO15VENDOREXPERIMENT環境變量)。因此對于Gophers們,搞 清楚vendor還是很必要的。本文就和大家一起來理解下vendor這個新feature。
?
一、vendor由來
Go第三方包依賴和管理的問題由來已久,民間知名的解決方案就有godep、?gb等。這次Go team在推出vendor前已經在Golang-dev group上做了長時間的調研,最終Russ Cox在Keith Rarick的proposal的基礎上做了改良,形成了Go 1.5中的vendor。
Russ Cox基于前期調研的結果,給出了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.
這四行詮釋在group中引起了強烈的討論,短小精悍的背后是理解上的不小差異。我們下面逐一舉例理解。
?
二、vendor基本樣例
Russ Cox詮釋中的第一條是vendor機制的基礎。粗獷的理解就是如果有如下這樣的目錄結構:
d/
?? vendor/
??? ????? p/
?????????? p.go
?? mypkg/
?? ?? ??? main.go
如果mypkg/main.go中有"import p",那么這個p就會被go工具解析為"d/vendor/p",而不是$GOPATH/src/p。
現在我們就來復現這個例子,我們在go15-vendor-examples/src/basic下建立如上目錄結構(其中go15-vendor-examples為GOPATH路徑):
$ls -R d/./d: mypkg/??? vendor/./d/mypkg: main.go./d/vendor: p/./d/vendor/p: p.go其中main.go代碼如下:
//main.go package mainimport "p"func main() {p.P() }p.go代碼如下:
//p.go package pimport "fmt"func P() {fmt.Println("P in d/vendor/p") }在未開啟vendor時,我們編譯d/mypkg/main.go會得到如下錯誤結果:
$ go build main.go
main.go:3:8: cannot find package "p" in any of:
??? /Users/tony/.bin/go15beta3/src/p (from $GOROOT)
??? /Users/tony/OpenSource/github.com/experiments/go15-vendor-examples/src/p (from $GOPATH)
錯誤原因很顯然:go編譯器無法找到package p,d/vendor下的p此時無效。
這時開啟vendor:export GO15VENDOREXPERIMENT=1,我們再來編譯執行一次:
$go run main.go
P in d/vendor/p
開啟了vendor機制的go tool在d/vendor下找到了package p。
也就是說擁有了vendor后,你的project依賴的第三方包統統放在vendor/下就好了。這樣go get時會將第三方包同時download下來,使得你的project無論被下載到那里都可以無需依賴目標環境而編譯通過(reproduce the building process)。
?
三、嵌套vendor
那么問題來了!如果vendor中的第三方包中也包含了vendor目錄,go tool是如何choose第三方包的呢?我們來看看下面目錄結構(go15-vendor-examples/src/embeded):
d/
?? vendor/
??? ????? p/
??? ??? ??? p.go
????????? q/
??????????? q.go
??????????? vendor/
?????????????? p/
???????????????? p.go
?? mypkg/
?? ?? ??? main.go
embeded目錄下出現了嵌套vendor結構:main.go依賴的q包本身還有一個vendor目錄,該vendor目錄下有一個p包,這樣我們就有了兩個p包。到底go工具會選擇哪個p包呢?顯然為了驗證一些結論,我們源文件也要變化一下:
d/vendor/p/p.go的代碼不變。
//d/vendor/q/q.go package qimport ("fmt""p" )func Q() {fmt.Println("Q in d/vendor/q")p.P() } //d/vendor/q/vendor/p/p.go package pimport "fmt"func P() {fmt.Println("P in d/vendor/q/vendor/p") } //mypkg/main.go package mainimport ("p""q" )func main() {p.P()fmt.Println("")q.Q() }目錄和代碼編排完畢,我們就來到了見證奇跡的時刻了!我們執行一下main.go:
$go run main.go P in d/vendor/pQ in d/vendor/q P in d/vendor/q/vendor/p可以看出main.go中最終引用的是d/vendor/p,而q.Q()中調用的p.P()則是d/vendor/q/vendor/p包的實現。go tool到底是如何在嵌套vendor情況下選擇包的呢?我們回到Russ Cox關于vendor詮釋內容的第二條:
?? When there are multiple possible resolutions,the most specific (longest) path wins.
這句話很簡略,但卻引來的巨大爭論。"longest path wins"讓人迷惑不解。如果僅僅從字面含義來看,上面main.go的執行結果更應該是:
P in d/vendor/q/vendor/pQ in d/vendor/q P in d/vendor/q/vendor/pd/vendor/q/vendor/p可比d/vendor/p路徑更long,但go tool顯然并未這么做。它到底是怎么做的呢?talk is cheap, show you the code。我們粗略翻看一下go tool的實現代碼:
在$GOROOT/src/cmd/go/pkg.go中有一個方法vendoredImportPath,這個方法在go tool中廣泛被使用:
// vendoredImportPath returns the expansion of path when it appears in parent. // If parent is x/y/z, then path might expand to x/y/z/vendor/path, x/y/vendor/path, // x/vendor/path, vendor/path, or else stay x/y/z if none of those exist. // vendoredImportPath returns the expanded path or, if no expansion is found, the original. // If no expansion is found, vendoredImportPath also returns a list of vendor directories // it searched along the way, to help prepare a useful error message should path turn // out not to exist. func vendoredImportPath(parent *Package, path string) (found string, searched []string)這個方法的doc講述的很清楚,這個方法返回所有可能的vendor path,以parent path為x/y/z為例:
x/y/z作為parent path輸入后,返回的vendor path包括:
x/y/z/vendor/path x/y/vendor/path x/vendor/path vendor/path這么說還不是很直觀,我們結合我們的embeded vendor的例子來說明一下,為什么結果是像上面那樣!go tool是如何resolve p包的!我們模仿go tool對main.go代碼進行編譯(此時vendor已經開啟)。
根據go程序的package init順序,go tool首先編譯p包。如何找到p包呢?此時的編譯對象是d/mypkg/main.go,于是乎parent = d/mypkg,經過vendordImportPath處理,可能的vendor路徑為:
d/mypkg/vendor d/vendor但只有d/vendor/下存在p包,于是go tool將p包resolve為d/vendor/p,于是下面的p.P()就會輸出:
P in d/vendor/p接下來初始化q包。與p類似,go tool對main.go代碼進行編譯,此時的編譯對象是d/mypkg/main.go,于是乎parent = d/mypkg,經過vendordImportPath處理,可能的vendor路徑為:
d/mypkg/vendor d/vendor但只有d/vendor/下存在q包,于是乎go tool將q包resolve為d/vendor/q,由于q包自身還依賴p包,于是go tool繼續對q中依賴的p包進行選擇,此時go tool的編譯對象變為了d/vendor/q/q.go,parent = d/vendor/q,于是經過vendordImportPath處理,可能的vendor路徑為:
d/vendor/q/vendor d/vendor/vendor d/vendor存在p包的路徑包括:
d/vendor/q/vendor/p d/vendor/p此時按照Russ Cox的詮釋2:choose longest,于是go tool選擇了d/vendor/q/vendor/p,于是q.Q()中的p.P()輸出的內容就是:
"P in d/vendor/q/vendor/p"
如果目錄結構足夠復雜,這個resolve過程也是蠻繁瑣的,但按照這個思路依然是可以分析出正確的包的。
另外vendoredImportPath傳入的parent x/y/z并不是一個絕對路徑,而是一個相對于$GOPATH/src的路徑。
BTW,上述測試樣例代碼在這里可以下載到。
?
四、第三和第四條
最難理解的第二條已經pass了,剩下兩條就比較好理解了。
The short form must always be used: no import path can? contain “/vendor/” explicitly.
這條就是說,你在源碼中不用理會vendor這個路徑的存在,該怎么import包就怎么import,不要出現import "d/vendor/p"的情況。vendor是由go tool隱式處理的。
Import comments are ignored in vendored packages.
go 1.4引入了canonical imports機制,如:
package pdf // import "rsc.io/pdf"
如果你引用的pdf不是來自rsc.io/pdf,那么編譯器會報錯。但由于vendor機制的存在,go tool不會校驗vendor中package的import path是否與canonical import路徑是否一致了。
?
五、問題
根據小節三中的分析,對于vendor中包的resolving過程類似是一個recursive(遞歸)過程。
main.go中的p使用d/vendor/p;而q.go中的p使用的是d/vendor/q/vendor/p,這樣就會存在一個問題:一個工程中存 在著兩個版本的p包,這也許不會帶來問題,也許也會是問題的根源,但目前來看從go tool的視角來看似乎沒有更好的辦法。Russ Cox期望大家良好設計工程布局,作為lib的包不攜帶vendor更佳。
這樣一個project內的所有vendor都集中在頂層vendor里面。就像下面這樣:
d/vendor/???q/p/… …mypkg1main.gomypkg2main.go… …另外Go vendor不支持第三方包的版本管理,沒有類似godep的Godeps.json這樣的存儲包元信息的文件。不過目前已經有第三方的vendor specs放在了github上,之前Go team的Brad Fizpatrick也在Golang-dev上征集過類似的方案,不知未來vendor是否會支持。
?
六、vendor vs. internal
在golang-dev有人提到:有了vendor,internal似乎沒用了。這顯然是混淆了internal和vendor所要解決的問題。
internal故名思議:內部包,不是對所有源文件都可見的。vendor是存儲和管理外部依賴包,更類似于external,里面的包都是copy自 外部的,工程內所有源文件均可import vendor中的包。另外internal在1.4版本中已經加入到go核心,是不可能輕易去除的,雖然到目前為止我們還沒能親自體會到internal 包的作用。
在《Go 1.5中值得關注的幾個變化》一文中我提到過go 1.5 beta1似乎“不支持”internal,beta3發布后,我又試了試看beta3是否支持internal包。
結果是beta3中,build依舊不報錯。但go list -json會提示錯誤:
"DepsErrors": [{"ImportStack": ["otherpkg","mypkg/internal/foo"],"Pos": "","Err": "use of internal package not allowed"}]難道真的要到最終go 1.5版本才會讓internal包發揮作用?
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的理解Go 1.5 vendor的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 干货:阅读跟踪 Java 源码的几个小技
- 下一篇: 初窥Go module