Vendor目錄介紹
隨着Go 1.5 release版本的發布,vendor目錄被添加到除了GOPATH
和GOROOT
之外的依賴目錄查找的解決方案。在Go 1.6之前,你需要手動的設置環境變量GO15VENDOREXPERIMENT=1
才可以使Go找到Vendor目錄,然而在Go 1.6之后,這個功能已經不需要配置環境變量就可以實現了。
Note,即使使用vendor,也必須在
GOPATH
中,在go的工具鏈中,你逃不掉GOPATH
的
GOPATH可以設置多個工程目錄,linux下用冒號分隔(必須用冒號,fish shell的空格分割會出錯,參見另一篇文章),windows下用分號分隔,但是go get 只會下載pkg到第一個目錄,但是編譯的時候會搜索所有的目錄。
go查找依賴包路徑的規則如下:
- 當前包下的
vendor
目錄。 - 向上級目錄查找,直到找到src下的
vendor
目錄。 - 在
GOPATH
下面查找依賴包。 - 在
GOROOT
目錄下查找
一些建議
在使用vendor中,給出如下建議:
- 一個library庫工程(不包含
main
的package)不應該在自己的版本控制中存儲外部的包在`vendor`目錄中,除非他們有特殊原因並且知道為什么要這么做。 - 在一個app應用中,(包含
main
的package),建議只有一個vendor
目錄在代碼庫一級目錄。
上面建議的原因如下:
- 在目錄結構中的每個包的實例,即使是同一個包的同一個版本,都會打到最終的二進制文件中,如果每個人都單獨的存儲自己的依賴包,會迅速導致生成文件的二進制爆發(binary bloat)
- 在一個目錄的某個pacage類型,並不兼容在同一個package但是在不同目錄的類型,即便是同一個版本的package,那意味着loggers,數據庫連接,和其他共享的實例都沒法工作。
舉個例子
工程目錄如下:
- $GOPATH/src/github.com/mattfarina/golang-broken-vendor - foo.go - vendor/ - a/ - b/ - vendor/a/
在這個例子中,兩個a
package都是完全一樣的,b package在代碼庫中保存了a package,在頂級應用代碼中也引用了a包。
文件foo.go
做了很簡單的事情:
func main() {
var it a.A
it = "foo"
b.Do(it)
}
那么問題來了,當我們build的時候,發現出問題了,返回了下面的錯誤:
$ GO15VENDOREXPERIMENT=1 go build
./foo.go:12: cannot use it (type "github.com/mattfarina/golang-broken-vendor/vendor/a".A) as type "github.com/mattfarina/golang-broken-vendor/vendor/b/vendor/a".A in argument to b.Do
你可以clone這個測試工程(https://github.com/mattfarina/golang-broken-vendor)到本地重現。
為什么用vendor目錄
如果我們已經使用GOPATH
去存儲packages了,問什么還需要使用vendor
目錄呢?這是一個很實戰的問題。
假如多個應用使用一個依賴包的不同版本?這個問題不只是Go應用,其他語言也會有這個問題。
vendor
目錄允許不同的代碼庫擁有它自己的依賴包,並且不同於其他代碼庫的版本,這就很好的做到了工程的隔離。
推薦
我們發現Glide是非常好的包管理解決方案,他將依賴包平展開存放在頂級vendor
目錄中,如果一個包被另一個程序引用了,那么這個包最好不要存儲外部依賴項。如果使用Glide,你可以在glide.yml
文件中指定依賴包,Glide會幫你管理,並使用正確的版本。
golang語言工程目錄結構:
- 設置GOPATH,這個環境變量指向你的projectDir(工程目錄),形如:GOPATH=/home/user/ext:/home/user/projectDir (可以設置多個工程目錄,linux下用冒號分隔,windows下用分號分隔)
- 創建工程文件夾projectDir
- 在projectDir下創建src目錄,(表示源代碼目錄)
- 在src下創建用以區分各個包的容器文件夾local, (本地包/庫的容器目錄,但它本身不屬於包的一部分)
- 在local下創建包pkgA目錄,(本地包/庫的目錄)
- 在pkgA下創建package source源代碼文件,這些文件的package都是pkgA,比如創建一個文件pkga.go,代碼如下:
package pkgA import "fmt" func TestPrint(){ fmt.Print("Hello world \n") }
寫完源代碼以后在src目錄下運行go install local/pkgA命令把包pkgA打包成.a文件(會在projectDir/pkg目錄下生成pkgA.a的目標文件)
在local下創建文件夾,取名helloDir。
在helloDir文件夾下創建帶有main函數的源代碼文件hello.go,代碼如下:
package main import ( "fmt" "local/pkgA" ) func main(){ fmt.Print("main package~\n") pkgA.TestPrint() }
在src下運行go install local/helloDir (會創建projectDir/bin目錄,並生成以helloDir 為文件名的可執行文件)。需要注意的是要生成可執行文件的話,go install后的文件夾下一定要有一個或多個屬於package main包的go源文件(即源代碼里第一行為 package main)。
最后projectDir目錄下的結構類似如下的形式:
.
├── bin
│ └── helloDir # executable
├── pkg
│ └── linux_amd64
│ └── local
│ └── pkgA.a # package object of pkgA
└── src
└── local
├── helloDir
│ └── hello.go # source code of package main, 可以有多個文件同時屬於 package main。 至少有一個屬於package main的文件才能編譯出可執行文件。
└── pkgA
└── pkga.go # package source
GOPATH環境變量為(go env | ack GOPATH):
GOPATH="/home/hzh/develop/go:/home/hzh/temp/go/projectDir"
======================================================================================================================
如果要使用glide來管理package,則在src目錄下運行 glide init,然后編輯 glide.yaml ,去掉本地庫的下載(使用 ignore),典型的glide.yaml文件如下:
package: . import: - package: github.com/pkg/errors version: ^0.8.0 ignore: - local/pkgA
另外,go build 和 go install 及 go run 的區別:
go install 是針對 package的,而 go build 和 go run 是針對某文件的。對於 go build 可以是任意文件,對於go run這個文件必須屬於package main。
go build 編譯package main時,生成的可執行文件在當前目錄,而 go install 編譯 package main 時,生成的可執行文件在項目的bin目錄下。
go build 和 go install 編譯普通package時(非package main),生成的庫都在項目的pkg目錄下。
go run 只可以編譯包含main()函數的那個.go文件,且立即執行文件。
go build 用於編譯我們指定的源碼文件或代碼包以及它們的依賴包。,但是注意如果用來編譯非命令源碼文件(即非可執行文件),即庫源碼文件,go build 執行完是不會產生任何結果的。這種情況下,go build 命令只是檢查庫源碼文件的有效性,只會做檢查性的編譯,而不會輸出任何結果文件。
注意,不管是以go build 或者 go install 還是 go run 的方式來編譯glide所管理的項目,所有的文件都必須位於其相應的package里,不允許某文件不位於任何package里,不然編譯不會成功。以下面的projectDir項目為例,hello.go屬於main package,如果將它移動到src下(不屬於任何目錄),此時用go build、go install 及 go run 都編譯不成功,提示找不到imported package(引用的外部package,也即vendor里的package)。
對於上面的項目,使用glide來管理的話,項目目錄結構為:
projectDir/
├── bin
│ └── helloDir
├── pkg
│ └── linux_amd64
│ ├── local
│ │ └── pkgA.a
│ └── vendor
│ └── github.com
│ └── pkg
│ └── errors.a
└── src
├── glide.lock
├── glide.yaml
├── local
│ ├── helloDir
│ │ └── hello.go
│ └── pkgA
│ └── pkga.go
└── vendor
GOPATH環境變量的值為 GOPATH="/home/hzh/develop/go:/home/hzh/temp/go/projectDir"
glide.yaml 的內容為:
package: . import: - package: github.com/pkg/errors version: ^0.8.0 ignore: - local/pkgA
hello.go的內容為:
package main import ( "fmt" "local/pkgA" "reflect" "github.com/pkg/errors" ) func main() { fmt.Print("main package~\n") pkgA.TestPrint() err := errors.New("hzh") fmt.Println("%T", err) fmt.Println(reflect.TypeOf(err).PkgPath()) }
pkga.go的內容為:
package pkgA import "fmt" func TestPrint() { fmt.Print("Hello world \n") }
編譯方法:
go build local/helloDir 或
go install local/helloDir 或
go run local/helloDir/hello.go
若由git來管理項目的版本,則.git 及 .gitignore 位於 src 目錄下,.gitignore 的內容必須包括 vendor/ 。
如果要使用glide來管理package,以下是最標准的目錄結構:
/home/hzh/hzh/dev/goo/src/projectDir ├── glide.lock ├── glide.yaml ├── local │ ├── helloDir │ │ └── hello.go │ └── pkgA │ └── pkga.go └── vendor
GOPATH環境變量的值為 GOPATH="/home/hzh/hzh/dev/go:/home/hzh/hzh/dev/goo"
glide.yaml 的內容為(直接在projectDir目錄下執行 glide init 命令):
package: projectDir
import:
- package: github.com/pkg/errors
hello.go的內容為:
package main import ( "fmt" "projectDir/local/pkgA" "reflect" "github.com/pkg/errors" ) func main() { fmt.Print("main package~\n") pkgA.TestPrint() err := errors.New("hzh") fmt.Println("%T", err) fmt.Println(reflect.TypeOf(err).PkgPath()) }
pkga.go的內容為:
package pkgA import "fmt" func TestPrint() { fmt.Print("Hello world \n") }
這種最標准方法的好處是在任何目錄里(切換到 /tmp 目錄,自己試試),都可以使用如下編譯方法來編譯任何項目:
go build projectDir/local/helloDir 或
go build projectDir/local/helloDir/hello.go 或
go install projectDir/local/helloDir 或
go run local/helloDir/hello.go 或 (先cd 到projectDir目錄)
go run helloDir/hello 或 (先cd 到projectDir/local目錄)
go run hello (先cd 到projectDir/local/helloDir目錄)
由上面的目錄結構可以看出,go的package路徑實際上是從 ${GOPATH}/src 開始算的,上面的最標准的例子中,package路徑即是從 projectDir 開始算的,中間的local目錄也算package的路徑,因此它是package 路徑(但不是package名);而projectDir屬於package的路徑起始位置,也屬於package路徑(但不是package名);而 helloDir 即是package路徑也是package名(但由於該package沒有被外部引用,所以package路徑與package名可以不相同,即 helloDir != main,其中main是package名)。真正的package名是由 .go 源文件聲明的,如果該package會被其它文件/package所引用,則申明的package名必須與路徑最后的目錄名相同,不然編譯通不過。強烈建議任何時候都保持package名與路徑的最后一個目錄名相同,不管該package是否被外部所引用,因為很難保證現在不被引用的package將來永遠不會被引用。