理解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穩定版還未釋出(最新訊息是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 main
import "p"
func main() {
p.P()
}
p.go程式碼如下:
//p.go
package p
import "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 q
import (
"fmt"
"p"
)
func Q() {
fmt.Println("Q in d/vendor/q")
p.P()
}
//d/vendor/q/vendor/p/p.go
package p
import "fmt"
func P() {
fmt.Println("P in d/vendor/q/vendor/p")
}
//mypkg/main.go
package main
import (
"p"
"q"
)
func main() {
p.P()
fmt.Println("")
q.Q()
}
目錄和程式碼編排完畢,我們就來到了見證奇蹟的時刻了!我們執行一下main.go:
$go run main.go
P in d/vendor/p
Q 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/p
Q in d/vendor/q
P in d/vendor/q/vendor/p
d/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/
… …
mypkg1
main.go
mypkg2
main.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包發揮作用?