1. 程式人生 > >理解Go 1.5 vendor

理解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 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.goparent = 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包發揮作用?