淺析Go項(xiàng)目中的依賴包管理與Go?Module常規(guī)操作
一.Go 構(gòu)建模式的演變
Go 程序由 Go 包組合而成的,Go 程序的構(gòu)建過程就是確定包版本、編譯包以及將編譯后得到的目標(biāo)文件鏈接在一起的過程。
Go 構(gòu)建模式歷經(jīng)了三個(gè)迭代和演化過程,分別是最初期的 GOPATH
、1.5 版本的 Vendor
機(jī)制,以及現(xiàn)在的 Go Module
。
1.1 GOPATH (初版)
Go 語言在首次開源時(shí),就內(nèi)置了一種名為 GOPATH 的構(gòu)建模式。
特點(diǎn):在這種構(gòu)建模式下,Go 編譯器可以在本地 GOPATH 環(huán)境變量配置的路徑下,搜尋 Go 程序依賴的第三方包。如果存在,就使用這個(gè)本地包進(jìn)行編譯;如果不存在,就會(huì)報(bào)編譯錯(cuò)誤
首先使用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 # 驗(yàn)證是否成功設(shè)置了 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 # 直接報(bào)錯(cuò)如下: 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
這個(gè)包,我們也同時(shí)假定當(dāng)前 GOPATH 環(huán)境變量配置的值為:
export GOPATH=/usr/local/goprojects:/root/go/
那么在 GOPATH
構(gòu)建模式下,Go
編譯器在編譯 Go
程序時(shí),就會(huì)在下面兩個(gè)路徑下搜索第三方依賴包是否存在:
/usr/local/goprojects/src/github.com/user/repo /root/go/src/github.com/user/repo
注意:如果你沒有顯式設(shè)置 GOPATH
環(huán)境變量,Go
會(huì)將 GOPATH
設(shè)置為默認(rèn)值,不同操作系統(tǒng)下默認(rèn)值的路徑不同,在 macOS
或 Linux
上,它的默認(rèn)值是 $HOME/go
。
當(dāng)本地找不到第三方依賴包的情況,我們該如何解決這個(gè)問題呢?
這個(gè)時(shí)候就需要讓 go get
登場了!
1.1.1 go get
在本地沒有找到程序的第三方依賴包,可以通過 go get 命令將本地缺失的第三方依賴包下載到本地,比如:
go get github.com/sirupsen/logrus
這里的go get
命令會(huì)下載第三方Go包及其依賴到本地的GOPATH
目錄下。并且go get
下載的包只是那個(gè)時(shí)刻各個(gè)依賴包的最新主線版本,這樣會(huì)給后續(xù) Go 程序的構(gòu)建帶來一些問題。比如,依賴包持續(xù)演進(jìn),可能會(huì)導(dǎo)致不同開發(fā)者在不同時(shí)間獲取和編譯同一個(gè) Go 包時(shí),得到不同的結(jié)果,也就是不能保證可重現(xiàn)的構(gòu)建(Reproduceable Build)。又比如,如果依賴包引入了不兼容代碼,程序?qū)o法通過編譯。
最后還有一點(diǎn),如果依賴包因引入新代碼而無法正常通過編譯,并且該依賴包的作者又沒用及時(shí)修復(fù)這個(gè)問題,這種錯(cuò)誤也會(huì)傳導(dǎo)到你的程序,導(dǎo)致你的程序無法通過編譯。
在 GOPATH 構(gòu)建模式下,Go 編譯器實(shí)質(zhì)上并沒有關(guān)注 Go 項(xiàng)目所依賴的第三方包的版本。但 Go 開發(fā)者希望自己的 Go 項(xiàng)目所依賴的第三方包版本能受到自己的控制,而不是隨意變化。所以 Go
核心開發(fā)團(tuán)隊(duì)引入了 Vendor
機(jī)制試圖解決上面的問題。
1.2 vendor 機(jī)制(中版)
Go 在 1.5 版本中引入 vendor 機(jī)制。所謂 vendor
機(jī)制,就是每個(gè)項(xiàng)目的根目錄下可以有一個(gè) vendor
目錄,里面存放了該項(xiàng)目的依賴的 package
。go build
的時(shí)候會(huì)先去 vendor
目錄查找依賴,如果沒有找到會(huì)再去 GOPATH
目錄下查找。
這樣的話,Go 編譯器會(huì)優(yōu)先感知和使用 vendor
目錄下緩存的第三方包版本,而不是 GOPATH
環(huán)境變量所配置的路徑下的第三方包版本。這樣,無論第三方依賴包自己如何變化,無論 GOPATH 環(huán)境變量所配置的路徑下的第三方包是否存在、版本是什么,都不會(huì)影響到 Go 程序的構(gòu)建。
如果使用 vendor
機(jī)制管理第三方依賴包,最佳實(shí)踐就是將 vendor
一并提交到代碼倉庫中。那么其他開發(fā)者下載你的項(xiàng)目后,就可以實(shí)現(xiàn)可重現(xiàn)的構(gòu)建。
下面這個(gè)目錄結(jié)構(gòu)就是為上面的代碼示例添加 vendor 目錄后的結(jié)果:
.
├── main.go
└── vendor/
├── github.com/
│ └── sirupsen/
│ └── logrus/
└── golang.org/
└── x/
└── sys/
└── unix/
在添加完 vendor 后,我們重新編譯 main.go,這個(gè)時(shí)候 Go 編譯器就會(huì)在 vendor 目錄下搜索程序依賴的 logrus 包以及后者依賴的 golang.org/x/sys/unix
包了.
注意:要想開啟 vendor 機(jī)制,你的 Go 項(xiàng)目必須位于 GOPATH 環(huán)境變量配置的某個(gè)路徑的 src 目錄下面。如果不滿足這一路徑要求,那么 Go 編譯器是不會(huì)理會(huì) Go 項(xiàng)目目錄下的 vendor 目錄的
不過 vendor 機(jī)制雖然一定程度解決了 Go 程序可重現(xiàn)構(gòu)建的問題,但對開發(fā)者來說,它的體驗(yàn)卻不那么好。一方面,Go
項(xiàng)目必須放在 GOPATH
環(huán)境變量配置的路徑下,龐大的 vendor
目錄需要提交到代碼倉庫,不僅占用代碼倉庫空間,減慢倉庫下載和更新的速度,而且還會(huì)干擾代碼評審,對實(shí)施代碼統(tǒng)計(jì)等開發(fā)者效能工具也有比較大影響。另外,你還需要手工管理 vendor
下面的 Go
依賴包,包括項(xiàng)目依賴包的分析、版本的記錄、依賴包獲取和存放等等。
為解決這個(gè)問題,Go
核心團(tuán)隊(duì)與社區(qū)將 Go 構(gòu)建的重點(diǎn)轉(zhuǎn)移到如何解決包依賴管理上。Go
社區(qū)先后開發(fā)了諸如 gb
、glide
、dep
等工具,來幫助 Go
開發(fā)者對 vendor
下的第三方包進(jìn)行自動(dòng)依賴分析和管理,但這些工具也都有自身的問題。
Go 核心團(tuán)隊(duì)基于社區(qū)實(shí)踐的經(jīng)驗(yàn)和教訓(xùn),推出了 Go 官方的最新解決方案:Go Module
。
1.3 Go Module(最新版)
Go 1.11 版本推出 modules
機(jī)制,簡稱 mod
,更加易于管理項(xiàng)目中所需要的模塊。
一個(gè) Go Module
是一個(gè) Go 包的集合。module
是有版本的,所以 module
下的包也就有了版本屬性。這個(gè) module
與這些包會(huì)組成一個(gè)獨(dú)立的版本單元,它們一起打版本、發(fā)布和分發(fā),。
在 Go Module
模式下,通常一個(gè)代碼倉庫對應(yīng)一個(gè) Go Module
。一個(gè) Go Module
的頂層目錄下會(huì)放置一個(gè) go.mod 文件,每個(gè) go.mod 文件會(huì)定義唯一一個(gè) module,也就是說 Go Module
與 go.mod
是一一對應(yīng)的。
并且其根目錄中包含 go.mod
文件,go.mod
文件定義了模塊的模塊路徑,它也是用于根目錄的導(dǎo)入路徑,以及它的依賴性要求。每個(gè)依賴性要求都被寫為模塊路徑和特定語義版本。
go.mod
文件所在的頂層目錄也被稱為 module
的根目錄,module
根目錄以及它子目錄下的所有 Go 包均歸屬于這個(gè) Go Module,這個(gè) module 也被稱為 main module。
從 Go 1.11
開始,Go
允許在 $GOPATH/src
外的任何目錄下使用 go.mod
創(chuàng)建項(xiàng)目。在 $GOPATH/src
中,為了兼容性,Go
命令仍然在舊的 GOPATH
模式下運(yùn)行。從 Go 1.13
開始,go.mod
模式將成為默認(rèn)模式。
二.創(chuàng)建Go Module
2.1 創(chuàng)建步驟
將基于當(dāng)前項(xiàng)目創(chuàng)建一個(gè) Go Module,通常有如下幾個(gè)步驟:
- 通過
go mod init [項(xiàng)目地址\庫地址]
創(chuàng)建 go.mod 文件,將當(dāng)前項(xiàng)目變?yōu)橐粋€(gè) Go Module; - 通過
go mod tidy
命令自動(dòng)更新當(dāng)前 module 的依賴信息; - 執(zhí)行
go build
,執(zhí)行新 module 的構(gòu)建。
2.2 簡單舉列
新建一個(gè)main.go文件,引入外部包 logrus
package main import "github.com/sirupsen/logrus" func main() { logrus.Println("hello, go module mode") }
我們通過 go mod ini
t 命令為這個(gè)項(xiàng)目創(chuàng)建一個(gè) Go Modul
e(這里我們使用的是 Go 版本最新版,Go 最新版默認(rèn)采用 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
在當(dāng)前項(xiàng)目目錄下創(chuàng)建了一個(gè) go.mod 文件,這個(gè) go.mod
文件將當(dāng)前項(xiàng)目變?yōu)榱艘粋€(gè) Go Module
,項(xiàng)目根目錄變成了 module 根目錄。go.mod
的內(nèi)容是這樣的.
module github.com/bigwhite/module-mode go 1.21.1
這個(gè) go.mod 文件現(xiàn)在處于初始狀態(tài),它的第一行內(nèi)容用于聲明 module 路徑(module path),一般是指定自己項(xiàng)目的git地址,最后一行是 Go 版本指示符,表示這個(gè) module 是在某個(gè)特定的 Go 版本的 module 語義的基礎(chǔ)上編寫的。
go mod init
命令日志輸出提示我們可以使用 go mod tidy
命令,添加 module 依賴以及校驗(yàn)和。go mod tidy
命令會(huì)掃描 Go 源碼,并自動(dòng)找出項(xiàng)目依賴的外部 Go Module 以及版本,下載這些依賴并更新本地的 go.mod
文件。我們按照這個(gè)提示執(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
我們看到,對于一個(gè)處于初始狀態(tài)的 module 而言,go mod tidy
分析了當(dāng)前 main module
的所有源文件,找出了當(dāng)前 main module
的所有第三方依賴,確定第三方依賴的版本,還下載了當(dāng)前 main module 的直接依賴包(比如 logrus),以及相關(guān)間接依賴包(直接依賴包的依賴,比如上面的 golang.org/x/sys 等)。
由 go mod tidy
下載的依賴 module 會(huì)被放置在本地的 module
緩存路徑下,默認(rèn)值為 $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
可以看到,當(dāng)前 module 的直接依賴 logrus,還有它的版本信息都被寫到了 go.mod
文件的 require
段中。而且,執(zhí)行完go mod tidy
后,當(dāng)前項(xiàng)目除了 go.mod
文件外,還多了一個(gè)新文件 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)命令維護(hù)的一個(gè)文件,它存放了特定版本 module
內(nèi)容的哈希值。這是 Go Module
的一個(gè)安全措施。當(dāng)將來這里的某個(gè) module 的特定版本被再次下載的時(shí)候,go 命令會(huì)使用 go.sum 文件中對應(yīng)的哈希值,和新下載的內(nèi)容的哈希值進(jìn)行比對,只有哈希值比對一致才是合法的,這樣可以確保你的項(xiàng)目所依賴的 module 內(nèi)容,不會(huì)被惡意或意外篡改。因此,我推薦你把 go.mod 和 go.sum 兩個(gè)文件與源碼,一并提交到代碼版本控制服務(wù)器上。
接下來,我們只需在當(dāng)前 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
整個(gè)過程的執(zhí)行步驟是這樣:go build
命令會(huì)讀取 go.mod
中的依賴及版本信息,并在本地 module 緩存路徑下找到對應(yīng)版本的依賴 module
,執(zhí)行編譯和鏈接。如果順利的話,我們會(huì)在當(dāng)前目錄下看到一個(gè)新生成的可執(zhí)行文件 module-mode
,執(zhí)行這個(gè)文件我們就能得到正確結(jié)果了。
三.深入理解 Go Module 構(gòu)建模式
Go 語言設(shè)計(jì)者在設(shè)計(jì) Go Module 構(gòu)建模式,來解決“包依賴管理”的問題時(shí),進(jìn)行了幾項(xiàng)創(chuàng)新,這其中就包括語義導(dǎo)入版本 (Semantic Import Versioning),以及和其他主流語言不同的最小版本選擇 (Minimal Version Selection) 等機(jī)制。
3.1 Go Module 的語義導(dǎo)入版本機(jī)制
在上面的例子中,我們看到 go.mod 的 require 段中依賴的版本號(hào),都符合 vX.Y.Z 的格式。在 Go Module 構(gòu)建模式下,一個(gè)符合 Go Module 要求的版本號(hào),由前綴 v 和一個(gè)滿足語義版本規(guī)范的版本號(hào)組成。例如,上面的 logrus module 的版本號(hào)是 v1.9.3,這就表示它的主版本號(hào)為 1,次版本號(hào)為 9,補(bǔ)丁版本號(hào)為 3.
語義版本號(hào)分成 3 部分:
- 主版本號(hào) (major)
- 次版本號(hào) (minor)
- 補(bǔ)丁版本號(hào) (patch)
Go 命令和 go.mod 文件都使用上面這種符合語義版本規(guī)范的版本號(hào),作為描述 Go Module 版本的標(biāo)準(zhǔn)形式。借助于語義版本規(guī)范,Go 命令可以確定同一 module 的兩個(gè)版本發(fā)布的先后次序,而且可以確定它們是否兼容。
按照語義版本規(guī)范,主版本號(hào)不同的兩個(gè)版本是相互不兼容的。而且,在主版本號(hào)相同的情況下,次版本號(hào)大都是向后兼容次版本號(hào)小的版本。補(bǔ)丁版本號(hào)也不影響兼容性。
而且,Go Module 規(guī)定:如果同一個(gè)包的新舊版本是兼容的,那么它們的包導(dǎo)入路徑應(yīng)該是相同的。
怎么理解呢?我們來舉個(gè)簡單示例。我們就以 logrus 為例,它有很多發(fā)布版本,我們從中選出兩個(gè)版本 v1.7.0 和 v1.8.1.。按照上面的語義版本規(guī)則,這兩個(gè)版本的主版本號(hào)相同,新版本 v1.8.1 是兼容老版本 v1.7.0 的。那么,我們就可以知道,如果一個(gè)項(xiàng)目依賴 logrus,無論它使用的是 v1.7.0 版本還是 v1.8.1 版本,它都可以使用下面的包導(dǎo)入語句導(dǎo)入 logrus 包:
import "github.com/sirupsen/logrus"
Go Module 創(chuàng)新性地給出了一個(gè)方法:將包主版本號(hào)引入到包導(dǎo)入路徑中,我們可以像下面這樣導(dǎo)入 logrus v2.0.0 版本依賴包:
import "github.com/sirupsen/logrus/v2"
這就是 Go 的“語義導(dǎo)入版本”機(jī)制,也就是說通過在包導(dǎo)入路徑中引入主版本號(hào)的方式,來區(qū)別同一個(gè)包的不兼容版本,這樣一來我們甚至可以同時(shí)依賴一個(gè)包的兩個(gè)不兼容版本:
import ( "github.com/sirupsen/logrus" logv2 "github.com/sirupsen/logrus/v2" )
不過到這里,你可能會(huì)問,v0.y.z 版本應(yīng)該使用哪種導(dǎo)入路徑呢?
按照語義版本規(guī)范的說法,v0.y.z 這樣的版本號(hào)是用于項(xiàng)目初始開發(fā)階段的版本號(hào)。在這個(gè)階段任何事情都有可能發(fā)生,其 API 也不應(yīng)該被認(rèn)為是穩(wěn)定的。Go Module 將這樣的版本 (v0) 與主版本號(hào) v1 做同等對待,也就是采用不帶主版本號(hào)的包導(dǎo)入路徑,這樣一定程度降低了 Go 開發(fā)人員使用這樣版本號(hào)包時(shí)的心智負(fù)擔(dān)。
Go 語義導(dǎo)入版本機(jī)制是 Go Module 機(jī)制的基礎(chǔ)規(guī)則,同樣它也是 Go Module 其他規(guī)則的基礎(chǔ)。
3.2 Go Module 的最小版本選擇原則
在前面的例子中,Go 命令都是在項(xiàng)目初始狀態(tài)分析項(xiàng)目的依賴,并且項(xiàng)目中兩個(gè)依賴包之間沒有共同的依賴,這樣的包依賴關(guān)系解決起來還是比較容易的。但依賴關(guān)系一旦復(fù)雜起來,比如像下圖中展示的這樣,Go 又是如何確定使用依賴包 C 的哪個(gè)版本的呢?
在這張圖中,myproject 有兩個(gè)直接依賴 A 和 B,A 和 B 有一個(gè)共同的依賴包 C,但 A 依賴 C 的 v1.1.0 版本,而 B 依賴的是 C 的 v1.3.0 版本,并且此時(shí) C 包的最新發(fā)布版為 C v1.7.0。這個(gè)時(shí)候,Go 命令是如何為 myproject 選出間接依賴包 C 的版本呢?
其實(shí),當(dāng)前存在的主流編程語言,以及 Go Module 出現(xiàn)之前的很多 Go 包依賴管理工具都會(huì)選擇依賴項(xiàng)的“最新最大 (Latest Greatest) 版本”,對應(yīng)到圖中的例子,這個(gè)版本就是 v1.7.0。
當(dāng)然了,理想狀態(tài)下,如果語義版本控制被正確應(yīng)用,并且這種“社會(huì)契約”也得到了很好的遵守,那么這種選擇算法是有道理的,而且也可以正常工作。在這樣的情況下,依賴項(xiàng)的“最新最大版本”應(yīng)該是最穩(wěn)定和安全的版本,并且應(yīng)該有向后兼容性。至少在相同的主版本 (Major Verion) 依賴樹中是這樣的
但我們這個(gè)問題的答案并不是這樣的。Go 設(shè)計(jì)者另辟蹊徑,在諸多兼容性版本間,他們不光要考慮最新最大的穩(wěn)定與安全,還要尊重各個(gè) module 的述求:A 明明說只要求 C v1.1.0,B 明明說只要求 C v1.3.0。所以 Go 會(huì)在該項(xiàng)目依賴項(xiàng)的所有版本中,選出符合項(xiàng)目整體要求的“最小版本”.
這個(gè)例子中,C v1.3.0 是符合項(xiàng)目整體要求的版本集合中的版本最小的那個(gè),于是 Go 命令選擇了 C v1.3.0,而不是最新最大的 C v1.7.0。并且,Go 團(tuán)隊(duì)認(rèn)為“最小版本選擇”為 Go 程序?qū)崿F(xiàn)持久的和可重現(xiàn)的構(gòu)建提供了最佳的方案。
即:對于導(dǎo)入路徑不同的包,則兩個(gè)包是同時(shí)被依賴的,導(dǎo)入路徑相同,則只能選擇依賴一個(gè),并且會(huì)選擇所有直接間接依賴這個(gè)包的版本的最高版本,而不是該包本身的最高版本,這是所有依賴這個(gè)包的其他包都能接受的最小版本,這樣可以保證服務(wù)整體的穩(wěn)定性。
3.3 Go 各版本構(gòu)建模式機(jī)制和切換
在 Go 1.11
版本中,Go 開發(fā)團(tuán)隊(duì)引入 Go Modules
構(gòu)建模式。這個(gè)時(shí)候,GOPATH
構(gòu)建模式與 Go Modules
構(gòu)建模式各自獨(dú)立工作,我們可以通過設(shè)置環(huán)境變量 GO111MODULE
的值在兩種構(gòu)建模式間切換。
然后,隨著 Go 語言的逐步演進(jìn),從 Go 1.11
到 Go 1.16
版本,不同的 Go 版本在 GO111MODULE
為不同值的情況下,開啟的構(gòu)建模式幾經(jīng)變化,直到 Go 1.16
版本,Go Module
構(gòu)建模式成為了默認(rèn)模式。
所以,要分析 Go 各版本的具體構(gòu)建模式的機(jī)制和切換,我們只需要找到這幾個(gè)代表性的版本就好了。
我這里將 Go 1.13
版本之前、Go 1.13
版本以及 Go 1.16
版本,在 GO111MODULE
為不同值的情況下的行為做了一下對比,這樣我們可以更好地理解不同版本下、不同構(gòu)建模式下的行為特性,下面我們就來用表格形式做一下比對:
四.設(shè)置 GO111MODULE
要啟用go module支持首先要設(shè)置環(huán)境變量GO111MODULE,通過它可以開啟或關(guān)閉模塊支持,它有三個(gè)可選值:off、on、auto,默認(rèn)值是auto。
- GO111MODULE=off禁用模塊支持,編譯時(shí)會(huì)從GOPATH和vendor文件夾中查找包。
- GO111MODULE=on啟用模塊支持,編譯時(shí)會(huì)忽略GOPATH和vendor文件夾,只根據(jù) go.mod下載依賴。
- GO111MODULE=auto,當(dāng)項(xiàng)目在$GOPATH/src外且項(xiàng)目根目錄有g(shù)o.mod文件時(shí),開啟模塊支持。
設(shè)置Go Model
# 臨時(shí)開啟 Go modules 功能 export GO111MODULE=on # 永久開啟 Go modules 功能 go env -w GO111MODULE=on
五.Go module 常用操作
5.1初始化項(xiàng)目
基于當(dāng)前項(xiàng)目創(chuàng)建一個(gè) Go Module,通常有如下幾個(gè)步驟:
- 通過
go mod init
項(xiàng)目名 創(chuàng)建go.mod
文件,將當(dāng)前項(xiàng)目變?yōu)橐粋€(gè)Go Module
; - 通過
go mod tidy
命令自動(dòng)更新當(dāng)前module
的依賴信息; - 執(zhí)行
go build
,執(zhí)行新module
的構(gòu)建。
然后會(huì)生成兩個(gè)文件go.mod
和go.sum
.
5.1.1 go.mod
go.mod文件記錄了項(xiàng)目所有的依賴信息,其結(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
禁止依賴包列表,不下載和引用哪些包(僅在當(dāng)前模塊為主模塊時(shí)生效)replace
替換依賴包列表和引用路徑(僅在當(dāng)前模塊為主模塊時(shí)生效)indirect
表示這個(gè)庫是間接引用進(jìn)來的。
5.1.2 replace
在國內(nèi)訪問golang.org/x的各個(gè)包都需要FQ,你可以在go.mod中使用replace替換成github上對應(yīng)的庫。
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 查看當(dāng)前項(xiàng)目所有包依賴
使用 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
我們可以在項(xiàng)目的 module 根目錄下,執(zhí)行帶有版本號(hào)的 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
當(dāng)然我們也可以使用萬能命令 go mod tidy來幫助我們降級,但前提是首先要用 go mod edit 命令,明確告知我們要依賴 v1.7.0 版本,而不是 v1.8.1,這個(gè)執(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 命令來清除這些沒用到的依賴項(xiàng):
go mod tidy
go mod tidy會(huì)自動(dòng)分析源碼依賴,而且將不再使用的依賴從 go.mod 和 go.sum 中移除。
5.4 引入主版本號(hào)大于 1 的三方庫
語義導(dǎo)入版本機(jī)制有一個(gè)原則:如果新舊版本的包使用相同的導(dǎo)入路徑,那么新包與舊包是兼容的。也就是說,如果新舊兩個(gè)包不兼容,那么我們就應(yīng)該采用不同的導(dǎo)入路徑。
按照語義版本規(guī)范,如果我們要為項(xiàng)目引入主版本號(hào)大于 1 的依賴,比如 v2.0.0,那么由于這個(gè)版本與 v1、v0 開頭的包版本都不兼容,我們在導(dǎo)入 v2.0.0 包時(shí),不能再直接使用 github.com/user/repo,而要使用像下面代碼中那樣不同的包導(dǎo)入路徑:
import github.com/user/repo/v2/xxx
也就是說,如果我們要為 Go 項(xiàng)目添加主版本號(hào)大于 1 的依賴,我們就需要使用“語義導(dǎo)入版本”機(jī)制,在聲明它的導(dǎo)入路徑的基礎(chǔ)上,加上版本號(hào)信息。我們以“向 module-mode 項(xiàng)目添加 github.com/go-redis/redis 依賴包的 v7 版本”為例,看看添加步驟。
首先,我們在源碼中,以空導(dǎo)入的方式導(dǎo)入 v7 版本的 github.com/go-redis/redis 包:
package main import ( _ "github.com/go-redis/redis/v7" // “_”為空導(dǎo)入 "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 升級依賴版本到一個(gè)不兼容版本
我們前面說了,按照語義導(dǎo)入版本的原則,不同主版本的包的導(dǎo)入路徑是不同的。所以,同樣地,我們這里也需要先將代碼中 redis 包導(dǎo)入路徑中的版本號(hào)改為 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 機(jī)制雖然誕生于 GOPATH 構(gòu)建模式主導(dǎo)的年代,但在 Go Module 構(gòu)建模式下,它依舊被保留了下來,并且成為了 Go Module 構(gòu)建機(jī)制的一個(gè)很好的補(bǔ)充。特別是在一些不方便訪問外部網(wǎng)絡(luò),并且對 Go 應(yīng)用構(gòu)建性能敏感的環(huán)境。
和GOPATH 構(gòu)建模式不同,Go Module 構(gòu)建模式下,我們再也無需手動(dòng)維護(hù) vendor 目錄下的依賴包了,Go 提供了可以快速建立和更新 vendor 的命令,我們還是以前面的module-mode 項(xiàng)目為例,通過下面命令為該項(xiàng)目建立 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)建了一份這個(gè)項(xiàng)目的依賴包的副本,并且通過 vendor/modules.txt 記錄了 vendor 下的 module 以及版本。
如果我們要基于 vendor 構(gòu)建,而不是基于本地緩存的 Go Module 構(gòu)建,我們需要在 go build 后面加上 -mod=vendor 參數(shù)。在 Go 1.14 及以后版本中,如果 Go 項(xiàng)目的頂層目錄下存在 vendor 目錄,那么 go build 默認(rèn)也會(huì)優(yōu)先基于 vendor構(gòu)建,除非你給 go build 傳入-mod=mod的參數(shù)。
通常我們直接使用 go module (非vendor) 模式即可滿足大部分需求。如果是那種開發(fā)環(huán)境受限,因無法訪問外部代理而無法通過 go 命令自動(dòng)解決依賴和下載依賴的環(huán)境下,我們通過 vendor 來輔助解決。
六、Go module 常用命令總結(jié)
6.1 常用 go mod命令
常用的go mod命令如下:
go mod download 下載依賴的module到本地cache(默認(rèn)為$GOPATH/pkg/mod目錄) go mod edit 編輯go.mod文件 go mod graph 打印模塊依賴圖 go mod init 初始化當(dāng)前文件夾, 創(chuàng)建go.mod文件 go mod tidy 增加缺少的module,刪除無用的module go mod vendor 將依賴復(fù)制到vendor下 go mod verify 校驗(yàn)依賴 go mod why 解釋為什么需要依賴
6.2 go get命令
在項(xiàng)目中執(zhí)行g(shù)o get命令可以下載依賴包,并且還可以指定下載的版本。
- 運(yùn)行g(shù)o get -u將會(huì)升級到最新的次要版本或者修訂版本(x.y.z, z是修訂版本號(hào), y是次要版本號(hào))
- 運(yùn)行g(shù)o get -u=patch將會(huì)升級到最新的修訂版本
- 運(yùn)行g(shù)o get package@version將會(huì)升級到指定的版本號(hào)version
如果下載所有依賴可以使用go mod download命令。
6.3 go mod edit
6.3.1 格式化
因?yàn)槲覀兛梢允謩?dòng)修改go.mod文件,所以有些時(shí)候需要格式化該文件。Go提供了一下命令:
go mod edit -fmt
6.3.2 添加依賴項(xiàng)
go mod edit -require=golang.org/x/text
6.3.3 移除依賴項(xiàng)
如果只是想修改go.mod文件中的內(nèi)容,那么可以運(yùn)行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 設(shè)置代理
7.1.1 打開模塊支持
go env -w GO111MODULE=on
7.1.2 取消代理
go env -w GOPROXY=direct
7.1.3 關(guān)閉包的有效性驗(yàn)證
go env -w GOSUMDB=off
7.1.4 設(shè)置不走 proxy 的私有倉庫或組,多個(gè)用逗號(hào)相隔(可選)
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 等),當(dāng)值列表中上一個(gè) Go module proxy 返回 404 或 410 錯(cuò)誤時(shí),Go 自動(dòng)嘗試列表中的下一個(gè),遇見 “direct” 時(shí)回源,遇見 EOF 時(shí)終止并拋出類似 “invalid version: unknown revision…” 的錯(cuò)誤。
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設(shè)置代理
八.項(xiàng)目中使用Go module
8.1 既有項(xiàng)目
如果需要對一個(gè)已經(jīng)存在的項(xiàng)目啟用go module,可以按照以下步驟操作:
- 在項(xiàng)目目錄下執(zhí)行g(shù)o mod init,生成一個(gè)go.mod文件。
- 執(zhí)行g(shù)o get,查找并記錄當(dāng)前項(xiàng)目的依賴,同時(shí)生成一個(gè)go.sum記錄每個(gè)依賴庫的版本和哈希值。
8.2 新項(xiàng)目
對于一個(gè)新創(chuàng)建的項(xiàng)目,我們可以在項(xiàng)目文件夾下按照以下步驟操作:
- 執(zhí)行g(shù)o mod init 項(xiàng)目名命令,在當(dāng)前項(xiàng)目文件夾下創(chuàng)建一個(gè)go.mod文件。
- 手動(dòng)編輯go.mod中的require依賴項(xiàng)或執(zhí)行g(shù)o get自動(dòng)發(fā)現(xiàn)、維護(hù)依賴。
九、查看Go的配置
$ go env //以JSON格式輸出 $ go env -json
以上就是淺析Go項(xiàng)目中的依賴包管理與Go Module常規(guī)操作的詳細(xì)內(nèi)容,更多關(guān)于go Module的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
golang生成指定位數(shù)的隨機(jī)數(shù)的方法
這篇文章主要介紹了golang生成指定位數(shù)的隨機(jī)數(shù)的方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10Go/C語言LeetCode題解997找到小鎮(zhèn)法官
這篇文章主要為大家介紹了Go語言LeetCode題解997找到小鎮(zhèn)的法官示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12golang使用excelize庫操作excel文件的方法詳解
Excelize是Go語言編寫的用于操作Office Excel文檔基礎(chǔ)庫,基于ECMA-376,ISO/IEC 29500國際標(biāo)準(zhǔn),下面這篇文章主要給大家介紹了關(guān)于golang使用excelize庫操作excel文件的相關(guān)資料,需要的朋友可以參考下2022-11-11golang cobra使用chatgpt qdrant實(shí)現(xiàn)ai知識(shí)庫
這篇文章主要為大家介紹了golang cobra使用chatgpt qdrant實(shí)現(xiàn)ai知識(shí)庫,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09