淺析Go項目中的依賴包管理與Go?Module常規(guī)操作
一.Go 構(gòu)建模式的演變
Go 程序由 Go 包組合而成的,Go 程序的構(gòu)建過程就是確定包版本、編譯包以及將編譯后得到的目標文件鏈接在一起的過程。
Go 構(gòu)建模式歷經(jīng)了三個迭代和演化過程,分別是最初期的 GOPATH
、1.5 版本的 Vendor
機制,以及現(xiàn)在的 Go Module
。
1.1 GOPATH (初版)
Go 語言在首次開源時,就內(nèi)置了一種名為 GOPATH 的構(gòu)建模式。
特點:在這種構(gòu)建模式下,Go 編譯器可以在本地 GOPATH 環(huán)境變量配置的路徑下,搜尋 Go 程序依賴的第三方包。如果存在,就使用這個本地包進行編譯;如果不存在,就會報編譯錯誤
首先使用go
多版本管理工具gvm
將 Go 版本到1.10.8:
# 如果沒有安裝gvm,使用如下命令安裝 bash < <(curl -s -S -L https://raw.githubusercontent.com/moovweb/gvm/master/binscripts/gvm-installer) gvm install go1.10.8 # 使用 GVM 安裝 Go 1.10.8 gvm use go1.10.8 # 切換到 Go 1.10.8 版本 go version # 驗證是否成功設置了 Go 1.10.8
這里給出了一段在 GOPATH 構(gòu)建模式下編寫的代碼:
package main import "github.com/sirupsen/logrus" func main() { logrus.Println("hello, gopath mode") }
然后使用Go 1.10.8編譯執(zhí)行如下:
# go build main.go # 直接報錯如下: main.go:3:8: cannot find package "github.com/sirupsen/logrus" in any of: /usr/local/go/src/github.com/sirupsen/logrus (from $GOROOT) /root/go/src/github.com/sirupsen/logrus (from $GOPATH)
那么 Go 編譯器在 GOPATH 構(gòu)建模式下,究竟怎么在 GOPATH 配置的路徑下搜尋第三方依賴包呢?
為了說清楚搜尋規(guī)則,先假定 Go 程序?qū)肓?nbsp;github.com/user/repo
這個包,我們也同時假定當前 GOPATH 環(huán)境變量配置的值為:
export GOPATH=/usr/local/goprojects:/root/go/
那么在 GOPATH
構(gòu)建模式下,Go
編譯器在編譯 Go
程序時,就會在下面兩個路徑下搜索第三方依賴包是否存在:
/usr/local/goprojects/src/github.com/user/repo /root/go/src/github.com/user/repo
注意:如果你沒有顯式設置 GOPATH
環(huán)境變量,Go
會將 GOPATH
設置為默認值,不同操作系統(tǒng)下默認值的路徑不同,在 macOS
或 Linux
上,它的默認值是 $HOME/go
。
當本地找不到第三方依賴包的情況,我們該如何解決這個問題呢?
這個時候就需要讓 go get
登場了!
1.1.1 go get
在本地沒有找到程序的第三方依賴包,可以通過 go get 命令將本地缺失的第三方依賴包下載到本地,比如:
go get github.com/sirupsen/logrus
這里的go get
命令會下載第三方Go包及其依賴到本地的GOPATH
目錄下。并且go get
下載的包只是那個時刻各個依賴包的最新主線版本,這樣會給后續(xù) Go 程序的構(gòu)建帶來一些問題。比如,依賴包持續(xù)演進,可能會導致不同開發(fā)者在不同時間獲取和編譯同一個 Go 包時,得到不同的結(jié)果,也就是不能保證可重現(xiàn)的構(gòu)建(Reproduceable Build)。又比如,如果依賴包引入了不兼容代碼,程序?qū)o法通過編譯。
最后還有一點,如果依賴包因引入新代碼而無法正常通過編譯,并且該依賴包的作者又沒用及時修復這個問題,這種錯誤也會傳導到你的程序,導致你的程序無法通過編譯。
在 GOPATH 構(gòu)建模式下,Go 編譯器實質(zhì)上并沒有關(guān)注 Go 項目所依賴的第三方包的版本。但 Go 開發(fā)者希望自己的 Go 項目所依賴的第三方包版本能受到自己的控制,而不是隨意變化。所以 Go
核心開發(fā)團隊引入了 Vendor
機制試圖解決上面的問題。
1.2 vendor 機制(中版)
Go 在 1.5 版本中引入 vendor 機制。所謂 vendor
機制,就是每個項目的根目錄下可以有一個 vendor
目錄,里面存放了該項目的依賴的 package
。go build
的時候會先去 vendor
目錄查找依賴,如果沒有找到會再去 GOPATH
目錄下查找。
這樣的話,Go 編譯器會優(yōu)先感知和使用 vendor
目錄下緩存的第三方包版本,而不是 GOPATH
環(huán)境變量所配置的路徑下的第三方包版本。這樣,無論第三方依賴包自己如何變化,無論 GOPATH 環(huán)境變量所配置的路徑下的第三方包是否存在、版本是什么,都不會影響到 Go 程序的構(gòu)建。
如果使用 vendor
機制管理第三方依賴包,最佳實踐就是將 vendor
一并提交到代碼倉庫中。那么其他開發(fā)者下載你的項目后,就可以實現(xiàn)可重現(xiàn)的構(gòu)建。
下面這個目錄結(jié)構(gòu)就是為上面的代碼示例添加 vendor 目錄后的結(jié)果:
.
├── main.go
└── vendor/
├── github.com/
│ └── sirupsen/
│ └── logrus/
└── golang.org/
└── x/
└── sys/
└── unix/
在添加完 vendor 后,我們重新編譯 main.go,這個時候 Go 編譯器就會在 vendor 目錄下搜索程序依賴的 logrus 包以及后者依賴的 golang.org/x/sys/unix
包了.
注意:要想開啟 vendor 機制,你的 Go 項目必須位于 GOPATH 環(huán)境變量配置的某個路徑的 src 目錄下面。如果不滿足這一路徑要求,那么 Go 編譯器是不會理會 Go 項目目錄下的 vendor 目錄的
不過 vendor 機制雖然一定程度解決了 Go 程序可重現(xiàn)構(gòu)建的問題,但對開發(fā)者來說,它的體驗卻不那么好。一方面,Go
項目必須放在 GOPATH
環(huán)境變量配置的路徑下,龐大的 vendor
目錄需要提交到代碼倉庫,不僅占用代碼倉庫空間,減慢倉庫下載和更新的速度,而且還會干擾代碼評審,對實施代碼統(tǒng)計等開發(fā)者效能工具也有比較大影響。另外,你還需要手工管理 vendor
下面的 Go
依賴包,包括項目依賴包的分析、版本的記錄、依賴包獲取和存放等等。
為解決這個問題,Go
核心團隊與社區(qū)將 Go 構(gòu)建的重點轉(zhuǎn)移到如何解決包依賴管理上。Go
社區(qū)先后開發(fā)了諸如 gb
、glide
、dep
等工具,來幫助 Go
開發(fā)者對 vendor
下的第三方包進行自動依賴分析和管理,但這些工具也都有自身的問題。
Go 核心團隊基于社區(qū)實踐的經(jīng)驗和教訓,推出了 Go 官方的最新解決方案:Go Module
。
1.3 Go Module(最新版)
Go 1.11 版本推出 modules
機制,簡稱 mod
,更加易于管理項目中所需要的模塊。
一個 Go Module
是一個 Go 包的集合。module
是有版本的,所以 module
下的包也就有了版本屬性。這個 module
與這些包會組成一個獨立的版本單元,它們一起打版本、發(fā)布和分發(fā),。
在 Go Module
模式下,通常一個代碼倉庫對應一個 Go Module
。一個 Go Module
的頂層目錄下會放置一個 go.mod 文件,每個 go.mod 文件會定義唯一一個 module,也就是說 Go Module
與 go.mod
是一一對應的。
并且其根目錄中包含 go.mod
文件,go.mod
文件定義了模塊的模塊路徑,它也是用于根目錄的導入路徑,以及它的依賴性要求。每個依賴性要求都被寫為模塊路徑和特定語義版本。
go.mod
文件所在的頂層目錄也被稱為 module
的根目錄,module
根目錄以及它子目錄下的所有 Go 包均歸屬于這個 Go Module,這個 module 也被稱為 main module。
從 Go 1.11
開始,Go
允許在 $GOPATH/src
外的任何目錄下使用 go.mod
創(chuàng)建項目。在 $GOPATH/src
中,為了兼容性,Go
命令仍然在舊的 GOPATH
模式下運行。從 Go 1.13
開始,go.mod
模式將成為默認模式。
二.創(chuàng)建Go Module
2.1 創(chuàng)建步驟
將基于當前項目創(chuàng)建一個 Go Module,通常有如下幾個步驟:
- 通過
go mod init [項目地址\庫地址]
創(chuàng)建 go.mod 文件,將當前項目變?yōu)橐粋€ Go Module; - 通過
go mod tidy
命令自動更新當前 module 的依賴信息; - 執(zhí)行
go build
,執(zhí)行新 module 的構(gòu)建。
2.2 簡單舉列
新建一個main.go文件,引入外部包 logrus
package main import "github.com/sirupsen/logrus" func main() { logrus.Println("hello, go module mode") }
我們通過 go mod ini
t 命令為這個項目創(chuàng)建一個 Go Modul
e(這里我們使用的是 Go 版本最新版,Go 最新版默認采用 Go Module 構(gòu)建模式)
$go mod init github.com/bigwhite/module-mode go: creating new go.mod: module github.com/bigwhite/module-mode go: to add module requirements and sums: go mod tidy
現(xiàn)在,go mod init
在當前項目目錄下創(chuàng)建了一個 go.mod 文件,這個 go.mod
文件將當前項目變?yōu)榱艘粋€ Go Module
,項目根目錄變成了 module 根目錄。go.mod
的內(nèi)容是這樣的.
module github.com/bigwhite/module-mode go 1.21.1
這個 go.mod 文件現(xiàn)在處于初始狀態(tài),它的第一行內(nèi)容用于聲明 module 路徑(module path),一般是指定自己項目的git地址,最后一行是 Go 版本指示符,表示這個 module 是在某個特定的 Go 版本的 module 語義的基礎上編寫的。
go mod init
命令日志輸出提示我們可以使用 go mod tidy
命令,添加 module 依賴以及校驗和。go mod tidy
命令會掃描 Go 源碼,并自動找出項目依賴的外部 Go Module 以及版本,下載這些依賴并更新本地的 go.mod
文件。我們按照這個提示執(zhí)行一下 go mod tidy
命令
$go mod tidy go: finding module for package github.com/sirupsen/logrus go: downloading github.com/sirupsen/logrus v1.9.3 go: found github.com/sirupsen/logrus in github.com/sirupsen/logrus v1.9.3 go: downloading golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 go: downloading github.com/stretchr/testify v1.7.0 go: downloading gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
我們看到,對于一個處于初始狀態(tài)的 module 而言,go mod tidy
分析了當前 main module
的所有源文件,找出了當前 main module
的所有第三方依賴,確定第三方依賴的版本,還下載了當前 main module 的直接依賴包(比如 logrus),以及相關(guān)間接依賴包(直接依賴包的依賴,比如上面的 golang.org/x/sys 等)。
由 go mod tidy
下載的依賴 module 會被放置在本地的 module
緩存路徑下,默認值為 $GOPATH[0]/pkg/mod
,Go 1.15 及以后版本可以通過 GOMODCACHE
環(huán)境變量,自定義本地 module 的緩存路徑。
執(zhí)行 go mod tidy 后,我們示例 go.mod 的內(nèi)容更新如下:
module github.com/bigwhite/module-mode go 1.21.1 require github.com/sirupsen/logrus v1.9.3 require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
可以看到,當前 module 的直接依賴 logrus,還有它的版本信息都被寫到了 go.mod
文件的 require
段中。而且,執(zhí)行完go mod tidy
后,當前項目除了 go.mod
文件外,還多了一個新文件 go.sum
,內(nèi)容是這樣的:
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
這同樣是由 go mod
相關(guān)命令維護的一個文件,它存放了特定版本 module
內(nèi)容的哈希值。這是 Go Module
的一個安全措施。當將來這里的某個 module 的特定版本被再次下載的時候,go 命令會使用 go.sum 文件中對應的哈希值,和新下載的內(nèi)容的哈希值進行比對,只有哈希值比對一致才是合法的,這樣可以確保你的項目所依賴的 module 內(nèi)容,不會被惡意或意外篡改。因此,我推薦你把 go.mod 和 go.sum 兩個文件與源碼,一并提交到代碼版本控制服務器上。
接下來,我們只需在當前 module 的根路徑下,執(zhí)行 go build 就可以完成 module 的構(gòu)建了!
$go build $$ls go.mod go.sum main.go module-mode $./module-mode INFO[0000] hello, go module mode
整個過程的執(zhí)行步驟是這樣:go build
命令會讀取 go.mod
中的依賴及版本信息,并在本地 module 緩存路徑下找到對應版本的依賴 module
,執(zhí)行編譯和鏈接。如果順利的話,我們會在當前目錄下看到一個新生成的可執(zhí)行文件 module-mode
,執(zhí)行這個文件我們就能得到正確結(jié)果了。
三.深入理解 Go Module 構(gòu)建模式
Go 語言設計者在設計 Go Module 構(gòu)建模式,來解決“包依賴管理”的問題時,進行了幾項創(chuàng)新,這其中就包括語義導入版本 (Semantic Import Versioning),以及和其他主流語言不同的最小版本選擇 (Minimal Version Selection) 等機制。
3.1 Go Module 的語義導入版本機制
在上面的例子中,我們看到 go.mod 的 require 段中依賴的版本號,都符合 vX.Y.Z 的格式。在 Go Module 構(gòu)建模式下,一個符合 Go Module 要求的版本號,由前綴 v 和一個滿足語義版本規(guī)范的版本號組成。例如,上面的 logrus module 的版本號是 v1.9.3,這就表示它的主版本號為 1,次版本號為 9,補丁版本號為 3.
語義版本號分成 3 部分:
- 主版本號 (major)
- 次版本號 (minor)
- 補丁版本號 (patch)
Go 命令和 go.mod 文件都使用上面這種符合語義版本規(guī)范的版本號,作為描述 Go Module 版本的標準形式。借助于語義版本規(guī)范,Go 命令可以確定同一 module 的兩個版本發(fā)布的先后次序,而且可以確定它們是否兼容。
按照語義版本規(guī)范,主版本號不同的兩個版本是相互不兼容的。而且,在主版本號相同的情況下,次版本號大都是向后兼容次版本號小的版本。補丁版本號也不影響兼容性。
而且,Go Module 規(guī)定:如果同一個包的新舊版本是兼容的,那么它們的包導入路徑應該是相同的。
怎么理解呢?我們來舉個簡單示例。我們就以 logrus 為例,它有很多發(fā)布版本,我們從中選出兩個版本 v1.7.0 和 v1.8.1.。按照上面的語義版本規(guī)則,這兩個版本的主版本號相同,新版本 v1.8.1 是兼容老版本 v1.7.0 的。那么,我們就可以知道,如果一個項目依賴 logrus,無論它使用的是 v1.7.0 版本還是 v1.8.1 版本,它都可以使用下面的包導入語句導入 logrus 包:
import "github.com/sirupsen/logrus"
Go Module 創(chuàng)新性地給出了一個方法:將包主版本號引入到包導入路徑中,我們可以像下面這樣導入 logrus v2.0.0 版本依賴包:
import "github.com/sirupsen/logrus/v2"
這就是 Go 的“語義導入版本”機制,也就是說通過在包導入路徑中引入主版本號的方式,來區(qū)別同一個包的不兼容版本,這樣一來我們甚至可以同時依賴一個包的兩個不兼容版本:
import ( "github.com/sirupsen/logrus" logv2 "github.com/sirupsen/logrus/v2" )
不過到這里,你可能會問,v0.y.z 版本應該使用哪種導入路徑呢?
按照語義版本規(guī)范的說法,v0.y.z 這樣的版本號是用于項目初始開發(fā)階段的版本號。在這個階段任何事情都有可能發(fā)生,其 API 也不應該被認為是穩(wěn)定的。Go Module 將這樣的版本 (v0) 與主版本號 v1 做同等對待,也就是采用不帶主版本號的包導入路徑,這樣一定程度降低了 Go 開發(fā)人員使用這樣版本號包時的心智負擔。
Go 語義導入版本機制是 Go Module 機制的基礎規(guī)則,同樣它也是 Go Module 其他規(guī)則的基礎。
3.2 Go Module 的最小版本選擇原則
在前面的例子中,Go 命令都是在項目初始狀態(tài)分析項目的依賴,并且項目中兩個依賴包之間沒有共同的依賴,這樣的包依賴關(guān)系解決起來還是比較容易的。但依賴關(guān)系一旦復雜起來,比如像下圖中展示的這樣,Go 又是如何確定使用依賴包 C 的哪個版本的呢?
在這張圖中,myproject 有兩個直接依賴 A 和 B,A 和 B 有一個共同的依賴包 C,但 A 依賴 C 的 v1.1.0 版本,而 B 依賴的是 C 的 v1.3.0 版本,并且此時 C 包的最新發(fā)布版為 C v1.7.0。這個時候,Go 命令是如何為 myproject 選出間接依賴包 C 的版本呢?
其實,當前存在的主流編程語言,以及 Go Module 出現(xiàn)之前的很多 Go 包依賴管理工具都會選擇依賴項的“最新最大 (Latest Greatest) 版本”,對應到圖中的例子,這個版本就是 v1.7.0。
當然了,理想狀態(tài)下,如果語義版本控制被正確應用,并且這種“社會契約”也得到了很好的遵守,那么這種選擇算法是有道理的,而且也可以正常工作。在這樣的情況下,依賴項的“最新最大版本”應該是最穩(wěn)定和安全的版本,并且應該有向后兼容性。至少在相同的主版本 (Major Verion) 依賴樹中是這樣的
但我們這個問題的答案并不是這樣的。Go 設計者另辟蹊徑,在諸多兼容性版本間,他們不光要考慮最新最大的穩(wěn)定與安全,還要尊重各個 module 的述求:A 明明說只要求 C v1.1.0,B 明明說只要求 C v1.3.0。所以 Go 會在該項目依賴項的所有版本中,選出符合項目整體要求的“最小版本”.
這個例子中,C v1.3.0 是符合項目整體要求的版本集合中的版本最小的那個,于是 Go 命令選擇了 C v1.3.0,而不是最新最大的 C v1.7.0。并且,Go 團隊認為“最小版本選擇”為 Go 程序?qū)崿F(xiàn)持久的和可重現(xiàn)的構(gòu)建提供了最佳的方案。
即:對于導入路徑不同的包,則兩個包是同時被依賴的,導入路徑相同,則只能選擇依賴一個,并且會選擇所有直接間接依賴這個包的版本的最高版本,而不是該包本身的最高版本,這是所有依賴這個包的其他包都能接受的最小版本,這樣可以保證服務整體的穩(wěn)定性。
3.3 Go 各版本構(gòu)建模式機制和切換
在 Go 1.11
版本中,Go 開發(fā)團隊引入 Go Modules
構(gòu)建模式。這個時候,GOPATH
構(gòu)建模式與 Go Modules
構(gòu)建模式各自獨立工作,我們可以通過設置環(huán)境變量 GO111MODULE
的值在兩種構(gòu)建模式間切換。
然后,隨著 Go 語言的逐步演進,從 Go 1.11
到 Go 1.16
版本,不同的 Go 版本在 GO111MODULE
為不同值的情況下,開啟的構(gòu)建模式幾經(jīng)變化,直到 Go 1.16
版本,Go Module
構(gòu)建模式成為了默認模式。
所以,要分析 Go 各版本的具體構(gòu)建模式的機制和切換,我們只需要找到這幾個代表性的版本就好了。
我這里將 Go 1.13
版本之前、Go 1.13
版本以及 Go 1.16
版本,在 GO111MODULE
為不同值的情況下的行為做了一下對比,這樣我們可以更好地理解不同版本下、不同構(gòu)建模式下的行為特性,下面我們就來用表格形式做一下比對:
四.設置 GO111MODULE
要啟用go module支持首先要設置環(huán)境變量GO111MODULE,通過它可以開啟或關(guān)閉模塊支持,它有三個可選值:off、on、auto,默認值是auto。
- GO111MODULE=off禁用模塊支持,編譯時會從GOPATH和vendor文件夾中查找包。
- GO111MODULE=on啟用模塊支持,編譯時會忽略GOPATH和vendor文件夾,只根據(jù) go.mod下載依賴。
- GO111MODULE=auto,當項目在$GOPATH/src外且項目根目錄有g(shù)o.mod文件時,開啟模塊支持。
設置Go Model
# 臨時開啟 Go modules 功能 export GO111MODULE=on # 永久開啟 Go modules 功能 go env -w GO111MODULE=on
五.Go module 常用操作
5.1初始化項目
基于當前項目創(chuàng)建一個 Go Module,通常有如下幾個步驟:
- 通過
go mod init
項目名 創(chuàng)建go.mod
文件,將當前項目變?yōu)橐粋€Go Module
; - 通過
go mod tidy
命令自動更新當前module
的依賴信息; - 執(zhí)行
go build
,執(zhí)行新module
的構(gòu)建。
然后會生成兩個文件go.mod
和go.sum
.
5.1.1 go.mod
go.mod文件記錄了項目所有的依賴信息,其結(jié)構(gòu)大致如下:
module dome go 1.18 require ( github.com/google/uuid v1.3.0 github.com/sirupsen/logrus v1.9.0 ) require ( github.com/kr/fs v0.1.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/tools/godep v0.0.0-20180126220526-ce0bfadeb516 // indirect golang.org/x/sys v0.3.0 // indirect golang.org/x/tools v0.4.0 // indirect )
其中,
module
用來模塊名稱require
用來定義依賴包及版本exclude
禁止依賴包列表,不下載和引用哪些包(僅在當前模塊為主模塊時生效)replace
替換依賴包列表和引用路徑(僅在當前模塊為主模塊時生效)indirect
表示這個庫是間接引用進來的。
5.1.2 replace
在國內(nèi)訪問golang.org/x的各個包都需要FQ,你可以在go.mod中使用replace替換成github上對應的庫。
replace ( golang.org/x/net => github.com/golang/net latest golang.org/x/tools => github.com/golang/tools latest golang.org/x/crypto => github.com/golang/crypto latest golang.org/x/sys => github.com/golang/sys latest golang.org/x/text => github.com/golang/text latest golang.org/x/sync => github.com/golang/sync latest )
5.1.3 go 查看當前項目所有包依賴
使用 go list -m all
可以查看到所有依賴列表,也可以使用 go list -json -m all
輸出 json格式的打印結(jié)果。
5.2 升級 / 降級版本
首先,查看依賴歷史版本
$go list -m -versions github.com/sirupsen/logrus github.com/sirupsen/logrus v0.1.0 v0.1.1 v0.2.0 v0.3.0 v0.4.0 v0.4.1 v0.5.0 v0.5.1 v0.6.0 v0.6.1 v0.6.2 v0.6.3 v0.6.4 v0.6.5 v0.6.6 v0.7.0 v0.7.1 v0.7.2 v0.7.3 v0.8.0 v0.8.1 v0.8.2 v0.8.3 v0.8.4 v0.8.5 v0.8.6 v0.8.7 v0.9.0 v0.10.0 v0.11.0 v0.11.1 v0.11.2 v0.11.3 v0.11.4 v0.11.5 v1.0.0 v1.0.1 v1.0.3 v1.0.4 v1.0.5 v1.0.6 v1.1.0 v1.1.1 v1.2.0 v1.3.0 v1.4.0 v1.4.1 v1.4.2 v1.5.0 v1.6.0 v1.7.0 v1.7.1 v1.8.0 v1.8.1
我們可以在項目的 module 根目錄下,執(zhí)行帶有版本號的 go get 命令:
$go get github.com/sirupsen/logrus@v1.7.0 go: downloading github.com/sirupsen/logrus v1.7.0 go get: downgraded github.com/sirupsen/logrus v1.8.1 => v1.7.0
當然我們也可以使用萬能命令 go mod tidy來幫助我們降級,但前提是首先要用 go mod edit 命令,明確告知我們要依賴 v1.7.0 版本,而不是 v1.8.1,這個執(zhí)行步驟是這樣的:
$go mod edit -require=github.com/sirupsen/logrus@v1.7.0 $go mod tidy go: downloading github.com/sirupsen/logrus v1.7.0
升級版本和降級版本依賴一樣,參照上面的操作即可,
5.3 刪除未使用的依賴
可以用 go mod tidy 命令來清除這些沒用到的依賴項:
go mod tidy
go mod tidy會自動分析源碼依賴,而且將不再使用的依賴從 go.mod 和 go.sum 中移除。
5.4 引入主版本號大于 1 的三方庫
語義導入版本機制有一個原則:如果新舊版本的包使用相同的導入路徑,那么新包與舊包是兼容的。也就是說,如果新舊兩個包不兼容,那么我們就應該采用不同的導入路徑。
按照語義版本規(guī)范,如果我們要為項目引入主版本號大于 1 的依賴,比如 v2.0.0,那么由于這個版本與 v1、v0 開頭的包版本都不兼容,我們在導入 v2.0.0 包時,不能再直接使用 github.com/user/repo,而要使用像下面代碼中那樣不同的包導入路徑:
import github.com/user/repo/v2/xxx
也就是說,如果我們要為 Go 項目添加主版本號大于 1 的依賴,我們就需要使用“語義導入版本”機制,在聲明它的導入路徑的基礎上,加上版本號信息。我們以“向 module-mode 項目添加 github.com/go-redis/redis 依賴包的 v7 版本”為例,看看添加步驟。
首先,我們在源碼中,以空導入的方式導入 v7 版本的 github.com/go-redis/redis 包:
package main import ( _ "github.com/go-redis/redis/v7" // “_”為空導入 "github.com/google/uuid" "github.com/sirupsen/logrus" ) func main() { logrus.Println("hello, go module mode") logrus.Println(uuid.NewString()) }
接下來我們通過 go get 獲取redis的v7版本:
$go get github.com/go-redis/redis/v7 go: downloading github.com/go-redis/redis/v7 v7.4.1 go: downloading github.com/go-redis/redis v6.15.9+incompatible go get: added github.com/go-redis/redis/v7 v7.4.1
5.5 升級依賴版本到一個不兼容版本
我們前面說了,按照語義導入版本的原則,不同主版本的包的導入路徑是不同的。所以,同樣地,我們這里也需要先將代碼中 redis 包導入路徑中的版本號改為 v8:
import ( _ "github.com/go-redis/redis/v8" "github.com/google/uuid" "github.com/sirupsen/logrus" )
接下來,我們再通過 go get 來獲取 v8 版本的依賴包:
$go get github.com/go-redis/redis/v8 go: downloading github.com/go-redis/redis/v8 v8.11.1 go: downloading github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f go: downloading github.com/cespare/xxhash/v2 v2.1.1 go get: added github.com/go-redis/redis/v8 v8.11.1
5.6 特殊情況:使用 vendor
vendor 機制雖然誕生于 GOPATH 構(gòu)建模式主導的年代,但在 Go Module 構(gòu)建模式下,它依舊被保留了下來,并且成為了 Go Module 構(gòu)建機制的一個很好的補充。特別是在一些不方便訪問外部網(wǎng)絡,并且對 Go 應用構(gòu)建性能敏感的環(huán)境。
和GOPATH 構(gòu)建模式不同,Go Module 構(gòu)建模式下,我們再也無需手動維護 vendor 目錄下的依賴包了,Go 提供了可以快速建立和更新 vendor 的命令,我們還是以前面的module-mode 項目為例,通過下面命令為該項目建立 vendor:
$go mod vendor
$tree -LF 2 vendor
vendor
├── github.com/
│ ├── google/
│ ├── magefile/
│ └── sirupsen/
├── golang.org/
│ └── x/
└── modules.txt
我們看到,go mod vendor 命令在 vendor 目錄下,創(chuàng)建了一份這個項目的依賴包的副本,并且通過 vendor/modules.txt 記錄了 vendor 下的 module 以及版本。
如果我們要基于 vendor 構(gòu)建,而不是基于本地緩存的 Go Module 構(gòu)建,我們需要在 go build 后面加上 -mod=vendor 參數(shù)。在 Go 1.14 及以后版本中,如果 Go 項目的頂層目錄下存在 vendor 目錄,那么 go build 默認也會優(yōu)先基于 vendor構(gòu)建,除非你給 go build 傳入-mod=mod的參數(shù)。
通常我們直接使用 go module (非vendor) 模式即可滿足大部分需求。如果是那種開發(fā)環(huán)境受限,因無法訪問外部代理而無法通過 go 命令自動解決依賴和下載依賴的環(huán)境下,我們通過 vendor 來輔助解決。
六、Go module 常用命令總結(jié)
6.1 常用 go mod命令
常用的go mod命令如下:
go mod download 下載依賴的module到本地cache(默認為$GOPATH/pkg/mod目錄) go mod edit 編輯go.mod文件 go mod graph 打印模塊依賴圖 go mod init 初始化當前文件夾, 創(chuàng)建go.mod文件 go mod tidy 增加缺少的module,刪除無用的module go mod vendor 將依賴復制到vendor下 go mod verify 校驗依賴 go mod why 解釋為什么需要依賴
6.2 go get命令
在項目中執(zhí)行g(shù)o get命令可以下載依賴包,并且還可以指定下載的版本。
- 運行g(shù)o get -u將會升級到最新的次要版本或者修訂版本(x.y.z, z是修訂版本號, y是次要版本號)
- 運行g(shù)o get -u=patch將會升級到最新的修訂版本
- 運行g(shù)o get package@version將會升級到指定的版本號version
如果下載所有依賴可以使用go mod download命令。
6.3 go mod edit
6.3.1 格式化
因為我們可以手動修改go.mod文件,所以有些時候需要格式化該文件。Go提供了一下命令:
go mod edit -fmt
6.3.2 添加依賴項
go mod edit -require=golang.org/x/text
6.3.3 移除依賴項
如果只是想修改go.mod文件中的內(nèi)容,那么可以運行go mod edit -droprequire=package path
,比如要在go.mod中移除golang.org/x/text包,可以使用如下命令:
go mod edit -droprequire=golang.org/x/text
關(guān)于go mod edit的更多用法可以通過go help mod edit查看。
七、Go Module 代理
7.1 GO 設置代理
7.1.1 打開模塊支持
go env -w GO111MODULE=on
7.1.2 取消代理
go env -w GOPROXY=direct
7.1.3 關(guān)閉包的有效性驗證
go env -w GOSUMDB=off
7.1.4 設置不走 proxy 的私有倉庫或組,多個用逗號相隔(可選)
go env -w GOPRIVATE=git.mycompany.com,github.com/my/private
7.1.5 國內(nèi)常用代理列表
提供者 | 地址 |
---|---|
官方全球代理 | https://proxy.golang.com.cn |
七牛云 | https://goproxy.cn |
阿里云 | https://mirrors.aliyun.com/goproxy/ |
GoCenter | https://gocenter.io |
百度 | https://goproxy.bj.bcebos.com/ |
“direct” 為特殊指示符,用于指示 Go 回源到模塊版本的源地址去抓取(比如 GitHub 等),當值列表中上一個 Go module proxy 返回 404 或 410 錯誤時,Go 自動嘗試列表中的下一個,遇見 “direct” 時回源,遇見 EOF 時終止并拋出類似 “invalid version: unknown revision…” 的錯誤。
7.1.6 官方全球代理
go env -w GOPROXY=https://proxy.golang.com.cn,direct go env -w GOPROXY=https://goproxy.io,direct go env -w GOSUMDB=gosum.io+ce6e7565+AY5qEHUk/qmHc5btzW45JVoENfazw8LielDsaI+lEbq6 go env -w GOSUMDB=sum.golang.google.cn
七牛云
go env -w GOPROXY=https://goproxy.cn,direct go env -w GOSUMDB=goproxy.cn/sumdb/sum.golang.org
阿里云
go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/,direct # GOSUMDB 不支持
GoCenter
go env -w GOPROXY=https://gocenter.io,direct # 不支持 GOSUMDB
百度
go env -w GOPROXY=https://goproxy.bj.bcebos.com/,direct # 不支持 GOSUMDB
Goland設置代理
八.項目中使用Go module
8.1 既有項目
如果需要對一個已經(jīng)存在的項目啟用go module,可以按照以下步驟操作:
- 在項目目錄下執(zhí)行g(shù)o mod init,生成一個go.mod文件。
- 執(zhí)行g(shù)o get,查找并記錄當前項目的依賴,同時生成一個go.sum記錄每個依賴庫的版本和哈希值。
8.2 新項目
對于一個新創(chuàng)建的項目,我們可以在項目文件夾下按照以下步驟操作:
- 執(zhí)行g(shù)o mod init 項目名命令,在當前項目文件夾下創(chuàng)建一個go.mod文件。
- 手動編輯go.mod中的require依賴項或執(zhí)行g(shù)o get自動發(fā)現(xiàn)、維護依賴。
九、查看Go的配置
$ go env //以JSON格式輸出 $ go env -json
以上就是淺析Go項目中的依賴包管理與Go Module常規(guī)操作的詳細內(nèi)容,更多關(guān)于go Module的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go返回int64類型字段超出javascript Number范圍的解決方法
這篇文章主要介紹了Go返回int64類型字段超出javascript Number范圍的解決方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-07-07