[Golang]包管理
目錄
- 1 GOPATH vs Go Modules
- 2 Go Modules、Go Module Proxy 和 goproxy.cn
- 3 Go Modules 相關知識
- 4 Go Modules 及其相關常用命令
- 5 Go Modules 實踐
- 6 總結
- 7 參考資料
本文是本人在探索 Go 最新的包管理 Go Modules
1 GOPATH vs Go Modules
在 Go1.5
之前用 GOPATH
以及 GOROOT
這兩個環境變數來管理包的位置,GOROOT
為 Go
的安裝目錄,以及編譯過程中使用到的系統庫存放位置,如fmt。Go1.5
到 Go1.7
開始穩定到 Vendor
方式,即依賴包需要放到 $GOPATH/src/vendor
目錄下,這樣每個專案都有自己的 vendor
目錄,但是如果依賴同樣的三方包,很容易造成資源重複,Go vendor 出現了幾種主流的管理工具,包括 godep
、govendor
golide
等。
在 Go1.11
之前,GOPATH
是開發時的工作目錄,其中包含三個子目錄:
- src目錄:存放go專案原始碼和依賴原始碼,包括使用
go get
下載的包 - bin目錄:通過使用
go install
命令將go build
編譯出的二進位制可執行檔案存放於此 - pkg目錄:go原始碼包編譯生成的lib檔案儲存的地方
在 Go1.11
之前,import
包時的搜尋路徑
GOROOT/src
: 該目錄儲存了Go標準庫程式碼(首先搜尋匯入包的地方)GOPATH/src
: 該目錄儲存了應用自身的各個包程式碼和第三方依賴的程式碼./vendor
:vendor 方式第三方依賴包(如果支援Vendor)
在 Unix 和類 Unix 系統上,GOPATH
預設值是 $HOME/go
,Go1.11 版本後,開啟 GO Modules 後,GOPATH
的作用僅僅為存放
依賴的目錄了。
在 Go
的 1.11
版本之前,GOPATH
是必需的,且所有的 Go
專案程式碼都要儲存在 GOPATH/src
目錄下,也就是如果想引用本地的包,你需要將包放在 $GOPATH/src
目錄下才能找得到。Go
的 1.11
版本之後,GO
官方引入了 Go Modules
,不僅僅方便的使用我們的依賴,而且還對依賴的版本進行了管理。
在Go1.11後通過 go mod vendor
和 -mod=vendor
來實現 Vendor 管理依賴方式。本來在 vgo
專案(Go Modules前身)是要完全放棄 vendor
,但是在社群反饋下還是保留了。總之就是在 Go.1.11 之後需要開啟 Go Modules 條件下才能使用 Vendor,具體地感興趣或還沿用了 Vendor 的朋友可以去了解下,不過建議以後僅使用 Go Modules 包管理方式了。
2 Go Modules、Go Module Proxy 和 goproxy.cn
Go Modules
是 Go 1.11
推出的功能模組,前身是 vgo
,成長於 Go 1.12
,豐富於 Go 1.13
是 Go 更好的一種模組依賴管理解決方案實現。
而 Go Module Proxy
是隨著 Go Modules
一起產生的模組代理協議,通過這個協議,我們可以實現 Go
模組代理,通過映象網站下載相關依賴模組。
proxy.golang.org
為 Go
官方模組代理網站,不翻牆中國使用者是無法訪問的,而 goproxy.cn
(官方推薦是使用 Go1.13
或以上版本)是七牛雲推出的非盈利性 Go
模組代理網站,為中國和世界上其他地方的 Gopher 們提供一個免費的、可靠的、持續線上的且經過 CDN 加速的模組代理,新增這個代理很簡單:
# 開啟 GO Modules 包管理方式
$ go env -w GO111MODULE=on
# 設定代理為 https://goproxy.cn
# 你也可以設定多個代理,通過逗號分隔開,模組從左至右設定的代理中查詢獲取
$ go env -w GOPROXY=https://goproxy.cn,direct
注意:模組可能是一個專案,專案下面可以包含很多包。
3 Go Modules 相關知識
3.1 語義化版本控制規範
Go Modules
是如何實現版本控制的呢?通過強制使用語義化版本控制規範,詳見 https://semver.org/lang/zh-CN/
示例,即我們釋出版本的時候必須按照官方指定的版本命名格式來發布,具體的:
- 你的版本
Tag
沒有遵循語義化版本控制規範那麼它就會忽略你的Tag
,然後根據你的Commit
時間和雜湊值再為你生成一個假定的符合語義化版本控制規範的版本號,比如v0.0.1-20180523231146-b3f5c0f6e5f1
。- 如v0.1.0,v1.0.0,v1.5.0-rc.1,
v
這個字元是必須的
- 如v0.1.0,v1.0.0,v1.5.0-rc.1,
Go Modules
預設認為,只要你的主版本號不變,那這個模組版本肯定就不包含重大變更,則我們import
的時候path
不會受到影響,比如v1.0.0
和v2.0.0
,就是一個重大版本變更,在編寫程式碼 import 模組的時候,v1版本的包名是github.com/xx/xx
,v2版本的包名就是github.com/xx/xx/v2
了,在我們使用go get
的時候也需要帶上完整的版本路徑才能匯入指定的版本。
3.2 go.mod
一個模組是通過go.mod
來定義的,也是標誌該專案是否啟用了 Go Modules
,如果存在該檔案,預設則啟動 Go Modules
,除非你設定 GO111MODULE=off
。該檔案描述了該模組的依賴、不依賴、依賴替換、當前模組名稱(路徑)、所要求的Go版本資訊,示例:
module my/thing
go 1.12
require other/thing v1.0.2
require new/thing/v2 v2.3.4
// 註釋:也可以用塊結構設定多個依賴模組
require (
new/thing v2.3.4
old/thing v1.2.3
github.com/my/repo v0.0.1-20180523231146-b3f5c0f6e5f1
)
exclude old/thing v1.2.3
replace bad/thing v1.4.5 => good/thing v1.4.5
其中:
- module, 定義模組的路徑(名稱)
- go, 設定期望的Go版本
- require, 在給定的版本或者更高的版本模組中,指定依賴一個特定版本
- exclude, 排除特定模組版本依賴
- replace, 將指定模組版本替換為其他模組版本
require
和 replace
僅僅在主模組的 go.mod
中應用,在依賴模組的 go.mod
中的 require 和 replace 將會忽略。另// indirect
,表示非直接依賴。go build
、go get
、go install
、go list
、go test
、go mod tidy
、go mod why
這些命令會去檢測本地模組的引用和存在,如果不存在會去下載相應模組,然後更新記錄到 go.mod
檔案。
replace
具體的作用就是將一個模組版本替換為另一個模組版本, =>
標誌前是待替換版本。
3.3 go.sum
go.sum
檔案的作用是為了驗證每個下載的模組是否與過去下載的模組匹配,並檢測模組是否被惡意篡改。比如你在開發過程中依賴了一個模組的某個版本,完成開發後,你上層版本管理平臺時只有go.mod
和go.sum
,如果其他人去使用該專案或者基於該專案開發,則需要在他本地重新下載相應的模組,這時go.sum
裡記錄的加密校驗和就可以校驗新環境下下載的模組是否與原始依賴保持一致。
在每一個模組的根目錄都有一個go.sum
與go.mod
相匹配,記錄go.mod
中每一個依賴模組的加密校驗和,校驗和的字首是h<N>
,h1表示採用SHA-256演算法得到校驗和,go.sum
的每一行格式為:
<模組路徑> <版本>[/go.mod] <校驗和>
// 示例:
// cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
// github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
// golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8=
如果下載的模組沒有包含在 go.sum
中,而且是一個公共可獲得的模組,Go 命令會去 Go 校驗資料庫(如預設配置的 sum.golang.org 或中國大陸官方校驗和資料庫 sum.golang.google.cn )查詢並獲取該模組的校驗和,如果下載的模組程式碼與校驗和不匹配,則報告不匹配相關資訊並退出,如果匹配,則將校驗和寫入 go.sum
檔案中。
3.4 GOPROXY、GONOPROXY、GOSUMDB、GONOSUMDB、GOPRIVATE
Go 命令可以根據 GOPROXY
環境變數的設定,從代理獲取模組或直接連線到原始碼管理伺服器。GOPROXY
的預設設定是 https://proxy.golang.org,direct
,這意味著嘗試獲取 Go 模組映象,如果代理報告它沒有該模組(HTTP錯誤404或410),則返回直接連線。如果 GOPROXY
設定為 "direct"
字串,則直接連線到原始碼管理伺服器下載模組。將 GOPROXY
設定為 "off"
不允許從任何源下載模組。你也可以設定多個代理,通過逗號(,
) 或者管道符號(|
)分隔開,模組從左至右設定的代理中查詢獲取,直到獲取模組成功或失敗返回。
通過設定 GOSUMDB
環境變數,可以配置模組校驗資料庫,如:
GOSUMDB="sum.golang.org" # 預設配置,URL 預設都是 https://,後跟資料庫地址
GOSUNDB="sum.golang.google.cn" # 中國大陸可訪問
GOSUMDB="sum.golang.org+<publickey>" # 使用除了sum.golang.org 和 sum.golang.google.cn 域名外其他需要給出公鑰
GOSUMDB="sum.golang.org+<publickey> https://sum.golang.org" #
GOSUMDB="off" # 關閉校驗,任何模組可以被使用
Go 命令預設是從公共的映象下載代理網站 proxy.golang.org
下載程式碼,然後通過公共校驗和資料庫 sum.golang.org
獲取模組校驗和實現校驗,但是有時候公司需要實現私有化依賴,即可以控制哪些模組可以不使用公共代理或校驗資料庫。
# 任何匹配*.corp.example.com為字首的模組都被視為私有模組,包括如git.crop.example.com/xxx, rsc.io/private/yyy
# 配置GORPIVATE過濾規則時,通過逗號分隔配置多個匹配路徑
$ go env -w GOPRIVATE=*.corp.example.com,rsc.io/private
為了對模組的下載和校驗進行細粒度的控制,GONOPROXY
和 GONOSUMDB
環境變數也支援同 GOPRIVATE
同樣的列表設定方式,也是配置統配模組或者指定模組,從而覆蓋 GOPRIVATE
對相關模組的作用,如果 GONOPROXY
設定成 none
,則所有的模型(公有,私有)都將從 GOPROXY
代理上下載,即 GOPRIVATE
設定無法生效。如:
GOPRIVATE=*.corp.example.com
GOPROXY=proxy.example.com
GONOPROXY=none
如果想要將某個模組不從 GOPROXY
中查詢下載,則設定 GONOPROXY
即可,並且也不校驗該模組,如:
GOPROXY=https://proxy.golang.org
GONOPROXY=gitlab.com/xxx
GONOSUMDB=$GONOPROXY
如果想禁止從 GOPROXY
上查詢下載模組,則可以配置 GONOPROXY=*
或者 GOPROXY=off
,不過這樣設定不會關掉對模組的校驗。
注: GOPRIVATE 、GONOSUMDB、GONOPROXY 的通配配置規則同Linux glob萬用字元語法一致,如
*
表示匹配任意長度任意字串。
4 Go Modules 及其相關常用命令
go get
命令會下載給定的匯入模組路徑所有的包,包括包的依賴模組
# 將會升級到最新的次要版本或者修訂版本(x.y.z, z是修訂版本號, y是次要版本號)
$ go get -u [URL]
# 將會升級到最新的次要版本
$ go get -u=patch [URL]
# 將不會校驗校驗碼,同 GOSUMDB=off 效果一致;另外可以下載來自非https域名的模組
$ go get -insecure [URL]
# 下載指定版本的模組,如最新版本是v2.2.0,將級下載v2.1.0
$ go get github.com/urfave/cli/[email protected]
# 拉取master分支最新提交
$ go get github.com/my/repo@master
# 拉取某個指定的提交
$ go get github.com/my/repo@772611b
go list -m all
: 檢視在編譯過程使用到所有直接和間接依賴項的最終版本go list -m -u all
: 檢視在編譯過程使用到所有直接和間接依賴項的最終版本以及他們可升級的次要的(minor)或補丁(patch)版本go get -u ./...
或go get -u=patch ./...
: 在模組根目錄執行,將所有直接和間接依賴項更新為最新的次要(minor)或補丁(patch)版本go build ./...
或go test ./...
: 在模組根目錄執行,編譯或測試模組中的所有包go clean -modcache
:刪除下載的快取內容,預設目錄為$HOME/go/mod
,整個目錄會刪除掉
注:如果沒有 go.mod 檔案,
go get
下載依賴後不會將版本依賴資訊記錄到go.mod
中。
go mod 相關:
$ go mod download 下載依賴的module到本地cache
$ go mod edit 編輯go.mod檔案
$ go mod graph 列印模組依賴圖
$ go mod init 在當前資料夾下初始化一個新的module, 建立go.mod檔案
$ go mod tidy 增加丟失的module,去掉未用的module
$ go mod vendor 將依賴複製到vendor目錄下
$ go mod verify 校驗依賴
$ go mod why 解釋為什麼需要依賴
具體使用:
$ go mod download [-x] [-json] [modules]
- 預設下載主模組依賴的所有模組到本地快取目錄中(預設為
$HOME/go/pkg/mod/cache
) -x
列印下載過程中執行的命令-json
將一系列json物件列印到標準輸出,描述每個下載的模組資訊,包括是否失敗、版本、模組路徑、校驗和值等
- 預設下載主模組依賴的所有模組到本地快取目錄中(預設為
$ go mod verify
- 驗證檢查當前模組(儲存在本地下載的源快取中)的依賴項在下載後是否未被修改。如果所有模組都未修改,verify會列印“all modules virfied”,否則它會報告哪些模組已更改,並導致“go mod”以非零狀態退出
$ go mod edit [editing flags] [go.mod]
- 主要是在命令列操作編輯go.mod檔案
-fmt
標誌表示格式化go.mod
檔案,不做除此之外其他更改操作-module=new-module-path
標誌 : 更改主模組的路徑(專案名稱),即第一行的module
內容-require=path@version
和-droprequire=path
: 新增和刪除require()
內容,但一般新增依賴我們更常用go get
將依賴自動更新到go.mod
中-exclude=path@version
和-dropexclude=path@version
:新增和刪除exclude
內容,如果以及新增已經存在,則不做任何操作。-replace=old[@v]=new[@v]
: 將舊模組替換為新模組-go=version
: 設定預期的Go語言版本-print
: 按格式化列印 go.mod 內容,不對 go.mod 做任何修改-json
: 按json格式列印 go.mod內容,如果需要知道專案的所以依賴用go list -m -json all
$ go mod graph
- 列印所有模組的依賴關係,除了主模組,其他模組依賴關係都帶有具體版本資訊
$ go mod init [module]
- 在當前目錄下建立一個模組路徑(模組名)為
[moudle]
的go.mod
,如果已經存在,則提示已經存在。
- 在當前目錄下建立一個模組路徑(模組名)為
$ go mod tidy [-v]
- 確保 go.mod 與 原始碼匹配,會添加當前編譯過程中包或者其他依賴所缺少的模組,會刪除沒有提供任何包無用的模組,還會新增一些缺失的校驗資訊到 go.sum中,移除無用的校驗資訊
-v
標誌將 tidy 過程中已刪除(沒有使用到)的模組資訊列印到標準錯誤
$ go mod vendor [-v]
- 這個命令是重置我們主模組vendor目錄,將所以編譯和測試依賴的包(不包括測試程式碼)全部拷貝一份到vendor目錄。
-v
標誌列印執行命令過程被拷貝的模組和包的名稱到標準錯誤
$ go mod why [-m] [-vendor] packages
- 顯示出
go mod graph
依賴關係中的一個最短依賴關係,比如go mod graph
展示出主模組依賴子模組1,子模組1依賴子模組2,則會全部展示,而如果想查某個模組或某些模組依賴了哪些,則可以用go mod why
。
- 顯示出
5 Go Modules 實踐
5.1 建立一個 Go Modules 專案
Go 官方 FAQs 上提到我們的專案沒有任何模組依賴是否有必要去新增一個 go.mod
檔案呢?它的建議是有必要的,這可以讓我們不再依賴 GOPATH
環境變數,也有利於模組的生態系統發展和交流,另外也可以作為你專案的一個宣告標誌,不過一切都是基於在 GO1.11 版本之上。那如何建立一個 Go Modules 專案呢?
- 首先我們要求
Go1.11
版本或以上,建議使用Go 1.13
版本或以上 - 進入我們專案的根目錄
$ cd <project path>
- 我們無需設定
GO111MODULE
環境變數,執行$ go mod init [your module path]
,如$ go mod init github.com/my/repo
、$ go mod init helloworld
,通常我們一般會結合版本控制系統(VCS)實現模組路徑的命名
- 然後就可以編寫程式碼,進行編譯了,hello.go
package main
import (
"fmt"
"rsc.io/quote"
)
func main() {
fmt.Println(quote.Hello())
}
- 編譯
$ go build -o hello.go
- 執行
$ ./hello
$ cat go.mod
module githu.com/my/repo
go 1.14
require rsc.io/quote v1.5.2
5.2 本地包依賴管理
在 Go Modules
沒有出來之前,在專案中 import 本地其他包都是通過設定好 GOPATH
,將專案路徑加入到 GOPATH
環境變數中,然後將我們的包放入 $GOPATH/src
下,這樣我們就可以找到本地依賴包。比如:
- {your project path}
- bin
- pkg
- src
- {package1 name} # 包名資料夾必須與包名一致
- package files
- {package2 name}
- package files
- main
- main.go
如何用 Go Modules
去實現本地包依賴呢?
在上一節,建立了一個最簡單的 Go Modules
專案,我們依賴了 rsc.io/quote
模組,這是一個從公共映象代理上可獲得的模組,但是如果我們自己定了內部的包,這個時候採用 Go Modules
方式如何去找到我們的包呢? 比如 pkg1 和 pkg2:
.
├── bin
├── cmd
│ └── hello
│ └── hello.go
├── go.mod
├── go.sum
├── pkg1
│ ├── pkg1_src.go
│ └── pkg1_test.go
└── pkg2
└── pkg2_src.go
其中 hello.go 、pkg1_src.go 、pkg1_test.go和 pkg2_src.go 內容分別為:
package main
import (
"fmt"
"rsc.io/quote"
"github.com/my/repo/pkg1"
)
func main() {
fmt.Println(quote.Hello())
pkg1.HelloPkg1()
}
package pkg1
import (
"fmt"
)
func HelloPkg1() string {
fmt.Println("Hello pkg1")
return "Hello pkg1"
}
package pkg1
import "testing"
func TestHello(t *testing.T) {
want := "Hello pkg1"
if got := HelloPkg1(); got != want {
t.Errorf("Hello() = %q, want %q", got, want)
}
}
package pkg2
import (
"fmt"
)
func HelloPkg2() {
fmt.Println("Hello pgk2")
}
通過在 hello.go
中使用我們的專案模組路徑 + 具體包路徑就可以引用到我們需要的本地包了。然後在專案根目錄編譯,將可執行檔案輸出到 bin
目錄:
# 在專案根目錄中編譯,./... 模式表示匹配在當前模組中所有的packages
# 注意:採用 ./... -o 只指定目錄,不能指定具體的生成物件名稱,因為你可能有多個可執行檔案一起生成
$ go build -o bin ./...
# 也可以單獨編譯我們的可執行檔案,並指定生成名稱
$ go build -o bin/hello_rename cmd/hello/hello.go
在 bin
目錄下預設生成 hello
名稱的可執行檔案,執行 ./bin/hello
:
Ahoy, world!
Hello pgk1
你也可以單獨將某個包編譯成 Go 靜態庫:
# 單獨編譯某個包,同樣的要找到這個包也需要使用專案模組路徑 + 具體包路徑
$ go build -buildmode=archive -o bin/libpkg1.a github.com/my/repo/pkg1
5.3 如何釋出我們的模組呢?
在完成我們 Go
模組後,如果需要提供給別人使用就需要釋出版本,結合版本控制系統(VCS),只要遵循 Go 的語義化版本控制規範,就可以很方便的釋出版本:
# 【step1】在釋出之前,建議執行 tidy,清除掉無關或者我們使用到但尚未新增進來的模組
$ go mod tidy
# 【step2】測試本專案模組中所有測試樣例,確保測試成功,go test all 會測試依賴在內的所有測試樣例
$ go test ./...
# 確保 go.sum 和 go.mod 檔案都一起提交到該版本中,go.sum 不是類似 nodejs 的 package-local.json 鎖檔案,更多地它可以幫助校驗本地下載地模組是否被篡改
# 【step3】版本提交
# git 提交操作,釋出v1.0.0版本
$ git add -A
$ git commit -m "hello: changes for v1.0.0"
$ git tag v1.0.0
$ git push origin v1.0.0
在 Go 版本釋出中,模組匯入路徑預設是省略了 v0、v1 主版本的。至於為什麼這樣設計,可以參考: https://github.com/golang/go/issues/24301 。
如果要釋出 v2 或者更高的版本?在官方的 FAQ 中很詳細的介紹了操作和一些建議,比如你有一個版本倉庫,已經打上了 v2.0.0 的標記,但是你還沒有采用 Go Modules 方式,建議你後續直接打上 v3,從而很清晰的而區分採用了 Go Modules 方式的版本。下面以釋出一個 v2+ 版本其中一種方式(另外參見 https://github.com/golang/go/wiki/Modules#releasing-modules-v2-or-higher )作為示例:
# 【step1】 將你的模組路徑帶上v2+資訊,如
$ go mod edit -module github.com/my/repo/v2
# 【step2】 更新你專案中使用了其他本地包的模組路徑,都加上v2,如我們上面的hello.go,則變為github.com/my/repo/v2/pkg1
# 【step3】 版本控制釋出 v2.x.x tag
5.4 遷移到 Go Modules 包管理
很多 Go 專案使用以前的老的包管理方式,Go 在遷移方面也做了很多工作,包括從以前的依賴管理自動遷移到 Go Modules 方式以及諸多遷移注意事項。這裡就不展開了,具體參見 https://github.com/golang/go/wiki/Modules#migrating-to-modules 。
當然,最簡單的遷移方式就是使用 Go1.13 或以上版本,重新組織你的專案和依賴,以及所有的匯入包路徑的修改,這相當於新初始化一個 Go Modules 專案。
6 總結
從最早的 GOPATH
到 Vendor
,再到 vgo
的出現, 最終 Go Modules
成熟,Go
的包依賴管理有了一個很大的進步,尤其是版本、資源和模組許可權的管理。Go Modules
還有更多的使用細節,這裡沒有去校驗,如果文章中有什麼理解錯誤,歡迎 Gopher
指正。
7 參考資料
注:
golang.google.cn
和golang.org
可替換,內容一致。golang.google.cn 在中國大陸無需翻牆即可訪問。
- https://stackoverflow.com/questions/37237036/how-should-i-use-vendor-in-go-1-6
- https://github.com/golang/go/wiki/Modules#how-do-i-use-vendoring-with-modules-is-vendoring-going-away
- https://tip.golang.org/cmd/go/#hdr-Modules_and_vendoring
- https://devopscon.io/blog/go-1-11-new-modules/
- https://developpaper.com/golang-1-5-to-golang-1-12-package-management-golang-vendor-to-go-mod/
- https://github.com/golang/go/wiki/Modules
- https://goproxy.cn/#Usage
- https://github.com/goproxy/goproxy.cn/blob/master/README.zh-CN.md
- https://github.com/golang/go/wiki/Modules#is-gosum-a-lock-file-why-does-gosum-include-information-for-module-versions-i-am-no-longer-using
- https://davidchan0519.github.io/2019/04/05/go-buildmode-c/
- https://github.com/go-modules-by-example/index/blob/master/009_submodules/README.md