golang使用vendor目錄來管理依賴包


Vendor目錄介紹

隨着Go 1.5 release版本的發布,vendor目錄被添加到除了GOPATHGOROOT之外的依賴目錄查找的解決方案。在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中,給出如下建議:

  1. 一個library庫工程(不包含main的package)不應該在自己的版本控制中存儲外部的包在`vendor`目錄中,除非他們有特殊原因並且知道為什么要這么做。
  2. 在一個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

我們發現Glide是非常好的包管理解決方案,他將依賴包平展開存放在頂級vendor目錄中,如果一個包被另一個程序引用了,那么這個包最好不要存儲外部依賴項。如果使用Glide,你可以在glide.yml文件中指定依賴包,Glide會幫你管理,並使用正確的版本。

 

 

 

golang語言工程目錄結構:

  1. 設置GOPATH,這個環境變量指向你的projectDir(工程目錄),形如:GOPATH=/home/user/ext:/home/user/projectDir (可以設置多個工程目錄,linux下用冒號分隔,windows下用分號分隔)
  2. 創建工程文件夾projectDir
  3. 在projectDir下創建src目錄,(表示源代碼目錄)
  4. 在src下創建用以區分各個包的容器文件夾local, (本地包/庫的容器目錄,但它本身不屬於包的一部分)
  5. 在local下創建包pkgA目錄,(本地包/庫的目錄)
  6. 在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將來永遠不會被引用。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM