《快學 Go 語言》第 16 課 —— 包管理 GOPATH 和 Vendor

碼洞發表於2018-12-27

到目前位置我們一直在編寫單檔案程式碼,只有一個 main.go 檔案。本節我們要開始朝完整的專案結構邁進,需要使用 Go 語言的模組管理功能來組織很多的程式碼檔案。

細數 Go 語言的歷史發展,模組管理經歷了三個重要的階段。第一階段是透過全域性的 GOPATH 來管理所有的第三方包,第二階段是透過 Vendor 機制將專案的依賴包區域性化,第三階段是 Go 語言的最新功能 Go Module。

本節我們重點講解前兩個階段,這兩個階段要求我們編寫程式碼時必須在 GOPATH 下面對應的包路徑目錄裡寫。第三個階段 Go Module 內容較新,也比較複雜需要另起一節單獨講解。

系統包路徑

Go 語言有很多內建包,內建包的使用需要使用者手工 import 進來。Go 語言的內建包都是已經編譯好的「包物件」,使用時編譯器不需要進行二次編譯。可以使用下面的命令檢視這些已經編譯好的包物件在哪裡。

// go sdk 安裝路徑
$ go env GOROOT
/usr/local/go
$ go env GOOS
darwin
$ go env GOARCH
amd64
$ ls /usr/local/go/darwin_amd64
total 22264
drwxr-xr-x   4 root  wheel      136 11  3 05:11 archive
-rw-r--r--   1 root  wheel   169564 11  3 05:06 bufio.a
-rw-r--r--   1 root  wheel   177058 11  3 05:06 bytes.a
drwxr-xr-x   7 root  wheel      238 11  3 05:11 compress
drwxr-xr-x   5 root  wheel      170 11  3 05:11 container
-rw-r--r--   1 root  wheel    93000 11  3 05:06 context.a
drwxr-xr-x  21 root  wheel      714 11  3 05:11 crypto
-rw-r--r--   1 root  wheel    24002 11  3 05:02 crypto.a
...


該命令顯示出來的字尾名為 .a 的檔案就是已經編譯好的包物件。

全域性管理 GOPATH

Go 語言的 GOPATH 路徑下存放了全域性的第三方依賴包,當我們在程式碼裡面 import 某個第三方包時,編譯器都會到 GOPATH 路徑下面來尋找。GOPATH 目錄可以指定多個位置,不過使用者一般很少這樣做。如果你沒有人工指定 GOPATH 環境變數,編譯器會預設將 GOPATH 指向的路徑設定為 ~/go 目錄。使用者可以使用下面的命令看看自己的 GOPATH 指向哪裡

go env GOPATH
/Users/qianwp/go


GOPATH 下有三個重要的子目錄,分別是 src、pkg 和 bin 目錄。src 目錄存放第三方包的原始碼,pkg 目錄存放編譯好的第三方包物件,bin 存放第三方包提供的二進位制可執行檔案。

《快學 Go 語言》第 16 課 —— 包管理 GOPATH 和 Vendor

當我們匯入第三方包時,編譯器優先尋找已經編譯好的包物件,如果沒有包物件,就會去原始碼目錄尋找相應的原始碼來編譯。使用包物件的編譯速度會明顯快於使用原始碼。

友好的包路徑

Go 語言允許包路徑帶有網站域名,這樣它就可以使用 go get 指令直接去相應的網站上拉去包程式碼。最常用的要數 github.com、gopkg.in、golang.org 這三個網址。

import "github.com/go-redis/redis"
import "golang.org/x/net"
import "gopkg.in/mgo.v2"
import "myhost.com/user/repo" // 個人提供的倉庫


Go 語言不存在官方維護的集中包倉庫,它將包的選擇分散到開源社群網站。使用量最大的要數 github.com,我們平時使用的大部分第三方包都是來源於此。也可以使用自己公司提供的程式碼倉庫,路徑名用上公司程式碼倉庫的域名即可。預設會使用 https 協議下載程式碼倉庫 ,可以使用 -insecure 引數切換到 http 協議。

模組的標準結構

瞭解模組結構的最好辦法就是看看別人的模組是怎麼寫的,這裡我們來觀察一下 mongo 包。使用下面的命令將 redis 的包下載本 GOPATH 目錄下

$ go get gopkg.in/mgo.v2


進入到 GOPATH 目錄下面的 src 子目錄尋找剛剛下載的 mongo 包,你會發現目錄層級和 go get 指令的包路徑正好一一對應起來,目錄下面還有更深的子目錄。

《快學 Go 語言》第 16 課 —— 包管理 GOPATH 和 Vendor

開啟程式碼中的任意一個檔案你可以發現程式碼中的 package 宣告的包名是 mgo,這個和當前的目錄名稱可以不一樣,不過當前目錄下所有的檔案都是這同一個包名 mgo。同時我們還注意到即使是包內程式碼引用,還是使用了全路徑來匯入而不是相對匯入,比如下圖的 bson,雖然同屬一個專案,但是它們好像根本就互不相識,要使用對方的的路徑全稱來打招呼。

《快學 Go 語言》第 16 課 —— 包管理 GOPATH 和 Vendor

當其它專案匯入這個包時,import 語句後面的路徑是 mongo 包的目錄路徑,而使用的包名卻是這個目錄下面程式碼中 package 語句宣告的包名 mgo。

package main

import "gopkg.in/mgo.v2"

func main() {
  session, err := mgo.Dial(url)
  ...
}

很不幸,例子中這個專案已經停止維護了,下面是它的文件中停止維護的宣告。

《快學 Go 語言》第 16 課 —— 包管理 GOPATH 和 Vendor

它已經由另一個社群專案接手。如果你要使用 mongo 的包,請使用

$ go get github.com/globalsign/mgo

編寫第一個模組

下面我們嘗試編寫第一個模組,這個模組是一個演算法模組,提供兩個方法,一個是計算斐波那契數,一個用來計算階乘。我們要將這個包放到 github.com 上,需要讀者在 github.com 上申請自己的賬戶,然後建立自己的專案名叫 mathy。我的 github id 是 pyloque,於是這個專案的包名就是 github.com/pyloque/mathy。第一步在 GOPATH 裡建立這個包目錄

$ mkdir -p ~/go/src/github.com/pyloque/mathy
$ cd ~/go/src/github.com/pyloque/mathy


好,現在我們進入了包的目錄下,開始編寫程式碼吧,首先建立 mathy.go 檔案,將下面的程式碼貼進去

package mathy

//  函式名大寫,其它的包才可以看的見
func Fib(n int) int64 {
    if n <= 1 {
        return 1
    }
    var s = make([]int64, n+1)
    s[0] = 1
    s[1] = 1
    for i := 2; i <= n; i++ {
        s[i] = s[i-1] + s[i-2]
    }
    return s[n]
}

func Fact(n int) int64 {
    if n <= 1 {
        return 1
    }
    var s int64 = 1
    for i := 2; i <= n; i++ {
        s *= int64(i)
    }
    return s
}


現在這個包的功能都齊全了,下面來編寫 main 函式使用它。我們可以去其它的任意空目錄下編寫下面的 main.go 檔案,但是不可以在當前目錄編寫,因為同一個目錄只能有同一個包名。比如我們在 mathy 目錄下面建立一個子目錄 cmd,將下面的程式碼貼到 cmd 目錄下的 main.go 檔案裡。執行 go run cmd/main.go 執行觀察結果

package main

import (
    "fmt"

    "github.com/pyloque/mathy"  // 引用剛剛建立的包名
)

func main() {
    fmt.Println(mathy.Fib(10))
    fmt.Println(mathy.Fact(10))
}

-------------
89
3628800


現在將程式碼提交到 github.com 上去吧,你最好已經比較熟悉 git 指令

$ git init
Initialized empty Git repository in /Users/qianwp/go/src/github.com/pyloque/mathy/.git/

$ git add --all

$ git commit -a -m 'first commit'
[master (root-commit7da8809] first commit
 2 files changed37 insertions(+)
 create mode 100644 cmd/main.go
 create mode 100644 mathy.go

$ git remote add origin 

$ git push origin master
Counting objects: 5, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (5/5), 555 bytes | 555.00 KiB/s, done.
Total 5 (delta 0), reused 0 (delta 0)
remote:
remote: Create a pull request for 'master' on GitHub by visiting:
remote:      new/master
remote:
To 
 * [new branch]      master -> master


開啟你的 github 專案頁看一看你剛剛提交的成果吧

《快學 Go 語言》第 16 課 —— 包管理 GOPATH 和 Vendor


這個專案提交到了 github.com 意味著全球的人都可以使用你的程式碼了,前提是人們願意使用。
現在你可以將本地的 mathy 資料夾刪除,然後執行一下 go get

$ go get github.com/pyloque/mathy


你會發現剛才刪掉的 mathy 目錄又出現了,因為 go get 指令會自動去 github.com 網站上拉取你剛才提交的專案程式碼。

Go 語言支援使用 . 和 .. 符號相對匯入,但是不推薦使用。官方表示相對匯入只應該用於本地測試,如果要正式釋出一定需要修改為絕對匯入。相對匯入可以不必將程式碼放在 GOPATH 裡面編寫,所以會方便本地測試。但是將程式碼放到 GOPATH 裡面寫又能產生多大障礙呢?總之就是不推薦使用相對匯入。

兩個包的包名一樣怎麼辦?

如果你的程式碼需要使用兩個包,這兩個包的路徑最後一個單詞是一樣的,那該如何分清使用的是那個包呢?為了解決這個問題,Go 語言支援匯入語句名稱替換功能

import pmathy "github.com/pyloque/mathy"
import omathy "github.com/other/mathy"

無名匯入

Go 語言還支援一種罕見的匯入語法可以將其它包的所有型別變數都匯入到當前的檔案中,在使用相關型別變數時可以省去包名字首。

package main

import "fmt"
import . "github.com/pyloque/mathy"

func main() {
  fmt.Println(Fib(10))
  fmt.Println(Fact(10))
}


但是這種用法很少見,而且非常不推薦使用,讀者可以當著沒看見完全不知道。

匿名匯入

Go 語言還支援匿名匯入,就是說你匯入了某個第三方包,但是不需要顯示使用它,這時就可以使用匿名匯入。什麼時候需要匯入某個包而不使用呢?這是因為 Go 語言的程式碼檔案中可以存在一個特殊的 init() 函式,它會在包檔案第一次被匯入的時候執行。

《快學 Go 語言》第 16 課 —— 包管理 GOPATH 和 Vendor

當我們使用資料庫驅動的時候就會經常遇到匿名匯入,第三方驅動包會在 init() 函式中將當前驅動註冊到全域性的驅動列表中,這樣透過特定的 URI 就可以識別並找到相應的驅動來使用。

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
)


當我們使用 Go 語言自帶的影像處理包時也會遇到匿名匯入,在對影像進行編碼解碼的時候需要根據不同的影像編碼選擇不同的邏輯。

import (  
    "image"
    _ "image/gif"
    _ "image/png"
    _ "image/jpeg"
)

包名和目錄名不一樣

Go 語言允許包名和當前的目錄名成不一樣,在匯入包的時候使用的是目錄路徑,但是在使用的時候應該使用目錄下的包名。所以你會看到匯入的路徑尾部和真正使用時的包名字首不一樣。

import "github.com/json-iterator/go"

var json = jsoniter.ConfigCompatibleWithStandardLibrary
json.Marshal(&data)


為什麼 json-iterator 會使用這樣奇怪的包路徑呢,因為它要支援多種語言的,直接將最後的目錄名改成語言的名稱更加易於辨識。

go get vs go build vs go install

Go 提供了三個比較的常用的指令用來進行全域性的包管理。

go build: 僅編譯。如果當前包裡有 main 包,就會生成二進位制檔案。如果沒有 main 包,build 指令僅僅用來檢查編譯是否可以透過,編譯完成後會丟棄編譯時生成的所有臨時包物件。這些臨時包包括自身的包物件以及所有第三方依賴包的包物件。如果指定 -i 引數,會將編譯成功的第三方依賴包物件安裝到 GOPATH 的 pkg 目錄。

go install:先編譯,再安裝。將編譯成的包物件安裝到 GOPATH 的 pkg 目錄中,將編譯成的可執行檔案安裝到 GOPATH 的 bin 目錄中。如果指定 -i 引數,還會安裝編譯成功的第三方依賴包物件。

go get:下載程式碼、編譯和安裝。安裝內容包括包物件和可執行檔案,但是不包括依賴包。

go get github.com/go-redis/redis

《快學 Go 語言》第 16 課 —— 包管理 GOPATH 和 Vendor

注意編譯過程中第三方包的 main 包是不可能被編譯的,安裝的物件也就不可能包括第三方依賴包的可執行檔案。

當我們使用 go run 指令來測試執行正在開發的程式時,如果發現啟動了很久,這時候可以考慮先執行 go build -i 指令,將編譯成功的依賴包都安裝到 GOPATH 的 pkg 目錄下,這樣再次執行 go run 指令就會快很多。

go build -i
go run main.go


當我們使用的第三方包已經比較陳舊,可以使用 go get -u 指令拉取最新的依賴包。

go get -u github.com/go-redis/redis

區域性管理 Vendor

當我們在本地要開發多個專案時,如果不同的專案需要依賴某個第三方包的不同版本,這時候僅僅透過全域性的 GOPATH 來存放第三方包是無解的。解決方法有一個,那就是需要在不同的專案裡設定不同的 GOPATH 變數來解決衝突問題。但是這還是不能解決一個重要的問題,那就是當我們的專案依賴了兩個第三方包,這兩個第三方包又同時依賴了另一個包的兩個不同版本,這時候就會再次發生衝突。這種多版本依賴有一個專業的名稱叫「鑽石型」依賴。

《快學 Go 語言》第 16 課 —— 包管理 GOPATH 和 Vendor

為了解決這個問題,Go 1.6 引入了 vendor 機制。這個機制非常簡單,就是在你自己專案的目錄下增加一個名字為 vendor 子目錄,將自己專案依賴的所有第三方包放到 vendor 目錄裡。這樣當你匯入第三方包的時候,優先去 vendor 目錄裡找你需要的第三方包,如果沒有,再去 GOPATH 全域性路徑下找。

《快學 Go 語言》第 16 課 —— 包管理 GOPATH 和 Vendor

然後每個第三方專案都會有自己的 vendor 子目錄,如此遞迴下去,可以想象,一個大型專案將會有一顆很深的依賴樹。不過實際上這顆依賴數沒你想象的那麼深,因為 Go 的第三方開源包普遍比較輕量級,依賴不是很多。畢竟 Go 語言已經將很多網際網路常用的工具包都內建了。

使用 vendor 有一個限制,那就是你不能將 vendor 裡面依賴的型別暴露到外面去,vendor 裡面的依賴包提供的功能僅限於當前專案使用,這就是 vendor 的「隔離沙箱」。正是因為這個沙箱才使得專案裡可以存在因為依賴傳遞導致的同一個依賴包的多個版本。同時這也意味著專案裡可能存在多份同一個依賴包,即使它們是同一個版本。比如你的包在 vendor 裡引入了某個第三方包 A,然後別人的專案在 vendor 裡引入你的包,同時它也引入第三方包 A。這就會導致生成的二進位制檔案變大,也會導致執行時記憶體變大,不過也無需擔心,這點代價對於服務端程式來說基本可以忽略不計。

講到這裡還有一個很重要的問題沒有解決,github 上有很多開源專案,這些專案都有多個版本號,我如何引入具體某一個版本呢?如果使用 go get 指令,它總是引入 master 分支的最新程式碼,它往往不是穩定的可靠程式碼。這就需要 Go 語言的依賴管理工具的支援了,它就好比 java 語言的 maven 工具,python 語言的 pip 工具。

Dep

《快學 Go 語言》第 16 課 —— 包管理 GOPATH 和 Vendor

Go 語言沒有內建 vendor 包管理工具,它需要第三方工具的支援。這樣的工具很多,目前最流行的要數 golang/dep 專案了,它差一點就被官方收納為內建工具了,很可惜!上圖是它的 Logo,圖中疊起來的箱子就是 dep 正在管理的各種第三方依賴包。使用它之前我們需要將 dep 工具安裝到 GOPATH 下面

$ go get github.com/golang/dep


同時需要將 ~/go/bin 目錄加入到環境變數 PATH 中,因為 dep 可執行檔案預設會安裝到 ~/go/bin 中。但是令人意外的是 dep 居然表示不能直接解決「鑽石型」依賴,這讓我感受到了它的危機,在 dep 中依賴包是扁平化的,vendor 不允許巢狀。如果出現了版本衝突,需要使用某種特殊手段來解決。

配置檔案

dep 管理的專案會有兩個配置檔案,分別是 Godep.toml 和 Godep.lock。Godep.toml 用於配置具體的依賴規則,裡面包含專案的具體版本號資訊。透過 toml 配置檔案,你即可以使用遠端的依賴包(github),也可以直接使用本地的依賴包(GOPATH)。還可以為依賴包指定別名,這樣就可以在程式碼裡使用和真實路徑不一樣的匯入路徑。當你需要切換依賴包的不同版本時,可以在 toml 配置檔案裡修改依賴的版本號,然後透過 dep ensure 指令來更新依賴項。

Gopkg.lock 是基於當前的 toml 檔案配置規則和專案程式碼來生成依賴的精確版本,它確定了 vendor 資料夾裡要下載的依賴項程式碼的目標版本。

dep init

該指令用於初始化當前的專案,它會靜態分析當前的專案程式碼(如有有的話),生成 Godep.toml 和 Godep.lock 依賴配置檔案,將依賴的專案程式碼下載到當前專案的 vendor 資料夾裡面。它會根據一定的策略來選擇最新的依賴包版本。如果自動策略生成的版本號不是你想要的,可以再修改配置檔案執行 dep ensure 來切換其它版本。

dep ensure

該指令會下載程式碼裡用到的新依賴項、移除當前專案程式碼裡不使用的依賴項。確保當前的依賴包程式碼和當前的專案程式碼配置處於完全一致的狀態。

dep ensure -update

更新 Godep.lock 檔案中的所有依賴項到最新版本。可以增加 一到多個包名引數,指定更新特定的依賴包。如果 toml 配置檔案限定了依賴包的版本範圍,那麼更新必須遵守 toml 規則的版本限制。

dep ensure -add github.com/a/b

增加並下載一個新的專案依賴包,可以指定依賴版本號。如 dep ensure -add github.com/a/b@master 或者 github.com/a/b@1.0.0

dep status

顯示當前專案的依賴狀態。

Dep 在使用起來比較簡單,但是其內部實現上是一個比較複雜的工具,鑑於篇幅限制,本節就不再繼續深入講解 Dep 了,以後有空再單獨開啟一篇來深入探討吧。我甚至覺得理解 Dep 已經變得沒有那麼必要,因為它已經被 Go 語言官方拋棄了,取而代之的解決方案是 Go Module。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31561269/viewspace-2286676/,如需轉載,請註明出處,否則將追究法律責任。

相關文章