一文搞懂Go語言的最新依賴管理:go mod的使用
2.1 背景
Go 依賴管理的演進經歷了以下 3 個階段:
目前被廣泛應用的是 Go Module,整個演進路線主要圍繞實現兩個目標來迭代發展:
- 不同環境 (項目) 依賴的版本不同;
- 控制依賴庫的版本。
2.2 Go 依賴管理的演進
2.2.1 GOPATH
GOPATH 是 Go 語言支持的一個環境變量,是 Go 項目的工作區。其目錄有以下 3 個結構 (需要手動創建文件夾):
文件夾 | 作用 |
---|---|
bin | 項目編譯的二進制文件 |
pkg | 項目編譯的中間產物 |
src | 項目源碼 |
- 項目代碼直接依賴
src
下的代碼; go get
下載最新版本的包到src
目錄下。
2.弊端
下面的場景就體現了 GOPATH 的弊端:項目A 和項B 依賴於某一 package 的不同版本 (分別爲 Pkg V1
和 Pkg V2
) 。而 src
下只能允許一個版本存在,那項目A 和項B 就無法保證都能編譯通過。
在 GOPATH 管理模式下,如果多個項目依賴同一個庫,則依賴該庫是同一份代碼,無法做到不同項目依賴同一個庫的不同版本。這顯然無法滿足實際開發中的項目依賴需求,爲了解決這個問題,Go Vendor 出現了。
2.2.2 Go Vendor
- 與 GOPATH 不同之處在於項目目錄下增加了
vendor
文件,所有依賴包以副本形式放在$ProjectRoot/vendor
下。 - 在 Vendor 機制下,如果當前項目存在 Vendor 目錄,會優先使用該目錄下的依賴;如果依賴不存在,則會從 GOPATH 中尋找。這樣,通過每個項目引入一份依賴的副本,解決了多個項目需要同一個 package 依賴的衝突問題。
2.弊端
但 Vendor 無法很好解決依賴包版本變動問題和一個項目依賴同一個包的不同版本的問題。
如圖項目A 依賴 Package B 和 Package C,而 Package B 和 Package C 又依賴了 Package D 的不同版本。通過 Vendor 的管理模式不能很好地控制對於 Package D 的依賴版本。一旦更新項目,有可能出現依賴衝突,導致編譯出錯。歸根到底: Vendor 不能很清晰地標識依賴的版本概念。
2.2.3 Go Module
- Go Module 是 Go 語言官方推出的依賴管理系統,解決了之前依賴管理系統存在的諸如無法依賴同一個庫的多個版本等問題。
- Go Module 自 Go1.11 開始引入,Go 1.16 默認開啓。可以在項目目錄下看到
go.mod
文件:
名稱 | 作用 |
---|---|
go.mod | 文件,管理依賴包版本 |
go get / go mod |
指令,管理依賴包 |
【終極目標】定義版本規則和管理項目依賴關係。和 Java 中的 Maven 作用是一樣的。
2.3 Go Module實踐
2.3.1 依賴管理三要素
要素 | 對於工具 |
---|---|
配置文件,描述依賴 | go.mod |
中心倉庫管理依賴庫 | Proxy |
本地工具 | go get / go mod |
2.3.2 依賴配置-go.mod
打開項目目錄下的 go.mod 文件,其文件結構主要分爲三部分:
【module 路徑 (上圖的“依賴管理基本單元”)】用來標識一個 module,從 module 路徑可以看出從哪裏找到該 module 。例如,如果以 github
爲前綴開頭,表示可以從 Github 倉庫找到該 module 。依賴包的源代碼由 Github 託管,如果項目的子包想被單獨引用,則需要通過單獨的 init go.mod
文件進行管理。
【原生庫】依賴的原生 Go SDK 版本。
【單元依賴】每個依賴單元用 module路徑 + 版本號
來唯一標識。
2.3.3 依賴配置-version
- GOPATH 和 Go Vendor 都是源碼副本方式依賴,沒有版本規則概念。
- 而 go.mod 爲了方便管理,定義了版本規則。分爲語義化版本和基於 commit 僞版本兩個版本。
1.語義化版本
${MAJOR}.${MINOR}.${PATCH}
- 1
如:V1.18.1、V1.8.0
名稱 | 含義 |
---|---|
MAJOR | 不同的MAJOR版本表示是不兼容的API。因此即使是同一個庫,MAJOR版本不同也會被認爲是不同的模塊 |
MINOR | 通常是新增函數或功能,向後兼容 |
PATCH | 一般是修復bug |
2.基於 commit 僞版本
每次提交 commit 後,Go 都會默認生成一個僞版本號:
v0.0.0-yyyymmddhhmmss-abcdefgh1234
- 1
如:v1.0.0-20220517152630-c38fb59326b7
名稱 | 含義 |
---|---|
v0.0.0 | 版本前綴和語義化版本是一樣的 |
yyyymmddhhmmss | 時間戳,提交Commit的時間 |
abcdefgh1234 | 校驗碼,包含12位的哈希前綴 |
2.3.4 依賴配置-indirect
2.3.2 節的 go.mod 文件圖中,細心觀察可以發現有些單元依賴帶有 // indirect
的後綴,這是一個特殊標識符,表示 go.mod 對應的當前 module 沒有直接導入的包,也就是非直接依賴 (即間接依賴) 。
例如,一個依賴關係鏈爲:A->B->C 。其中,A->B 是直接依賴;而 A->C 是間接依賴。
2.3.5 依賴配置-incompatible
2.3.2 節的 go.mod 文件圖中,細心觀察可以發現有些單元依賴帶有 +incompatible
的後綴,這也是一個特殊標識符。對於 MAJOR 主版本在 V2 及以上的模塊,go.mod 會在模塊路徑增加 /vN
後綴 (如下圖中 example/lib5/v3 v3.0.2
)。這能讓 Go Module 按照不同的模塊來處理同一個項目不同 MAJOR 主版本的依賴。
- 由於 Go Module 是在 Go 1.11 才實驗性地引入,所以在這個更新提出之前,已經有一些倉庫打上了 V2 或者更高版本的 tag 了。
- 爲了兼容這部分倉庫,對於沒有 go.mod 文件並且 MAJOR 主版本在 V2 及以上的依賴,會在版本號後加上
+incompatible
後綴。表示可能會存在不兼容的源代碼。
2.3.6 依賴配置-依賴圖
如下圖所示,Main 項目依賴項目A 和項目B ,且項目A 和項目B 分別依賴項目C 的 v1.3 和 v1.4 版本。最終編譯時,Go 所使用的項目C 的版本爲:v1.4 。
【總結】Go 選擇最低的兼容版本。
2.3.7 依賴分發-回源
- 依賴分發,即依賴從何處下載、如何下載的問題。
- Go 的依賴絕大部分託管在 GitHub 上。Go Module 系統中定義的依賴,最終都可以對應到 GitHub 中某一項目的特定提交 (commit) 或版本。
- 對於 go.mod 中定義的依賴,則直接可以從對應倉庫中下載指定依賴,從而完成依賴分發。
2.弊端
直接使用 GitHub 倉庫下載依賴存在一些問題:
- 首先,無法保證構建穩定性。代碼作者可以直接在 GitHub 上增加/修改/刪除軟件版本。
- 無法保證依賴可用性。代碼作者可以直接在 GitHub 上刪除代碼倉庫,導致依賴不可用。
- 第三,如果所有人都直接從 GitHub 上獲取依賴,會導致 GitHub 平臺負載壓力。
3.解決方案-Proxy
- Go Proxy 就是解決上述問題的方案。Go Proxy 是一個服務站點,它會緩存 GitHub 中的代碼內容,緩存的代碼版本不會改變,並且在 GitHub 作者刪除了代碼之後也依然可用,從而實現了 “immutability” (不變性) 和 “available” (可用的) 的依賴分發。
- 使用 Go Proxy 後,構建時會直接從 Go Proxy 站點拉取依賴。如下圖所示。
2.3.8 GOPROXY的使用
-
Go Module 通過 GOPROXY 環境變量控制如何使用 Go Proxy 。
-
GOPROXY 是一個 Go Proxy 站點 URL 列表。
GOPROXY = "https://proxy1.cn, https://proxy2.cn, direct"
- 1
-
上述代碼中,
direct
表示源站 (如 GitHub) ,proxy1
proxy2
是兩個URL 站點。依賴尋址路徑爲:優先從proxy1
下載依賴,如果proxy1
不存在,再從proxy2
尋找,如果proxy2
不存在,則會回源到源站直接下載依賴,並緩存到 Go Proxy 站點中 (這種設計思路和 Redis 緩存與 MySQL 數據庫一模一樣)。
2.3.9 工具-go get
go get example.org/pkg +...
- 1
後面跟不同的指令能實現不同的功能:
指令 | 功能 |
---|---|
@update | 默認 |
@none | 刪除依賴 |
@v1.1.2 | 下載指定tag版本,語義版本 |
@23dfdd5 | 下載特定的commit版本 |
@master | 下載分支的最新commit |
2.3.10 工具-go mod
指令 | 功能 |
---|---|
init | 初始化,創建go.mod文件 |
download | 下載模塊到本地緩存 |
tidy | 增加需要的依賴,刪除不需要的依賴 |
在實際開發中,儘量提交之前執行下 go tidy
,減少構建時無效依賴包的拉取。
2.4 go mod使用
2.4.1 設置GO111MODULE
Win + R 輸入 cmd 打開命令行,輸入:
go env
即可看到 GO111MODULE (默認情況是空的):
GO111MODULE 有三個值:off、on 和 auto (默認值)
GO111MODULE=off
:go命令行將不會支持module功能,尋找依賴包的方式將會沿用舊版本那種通過vendor目錄或者GOPATH模式來查找。GO111MODULE=on
:go命令行會使用modules,而一點也不會去GOPATH目錄下查找。GO111MODULE=auto
:默認值,go命令行將會根據當前目錄來決定是否啓用module功能。這種情況下可以分爲兩種情形:- 當前目錄在GOPATH/src之外且該目錄包含go.mod文件
- 當前文件在包含go.mod文件的目錄下面。
【注】
- 在使用go module時,GOPATH是無意義的。不過它仍然會把下載的依賴存儲在
$GOPATH/pkg/mod
中,也會把go install
的結果放在$GOPATH/bin
中。 - 當module功能啓用時,依賴包的存放位置變更爲
$GOPATH/pkg
。允許同一個package多個版本並存,且多個項目可以共享緩存的module。
設置的命令如下:
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct
可在命令行中輸入:go env
查看 GO111MODULE=on
。
2.4.2 清空所有GOPATH
開啓 go mod 之後,並不能與 GOPATH 共存。必須把項目從 GOPATH 中移除,否則會報 $GOPATH/go.mod exists but should not
的錯誤。
在 Goland 中,移除項目所有 GOPATH 的操作如下:
清空 GOPATH 之後,在單元測試模式下,同一個包下不同文件函數調用報錯爲 undefined
的問題也會解決。
2.4.3 在新項目中創建go mod
注意:如果你的項目根目錄下已經有 go.mod
文件,可以不需要創建 go.mod
文件。
爲了演示如何管理依賴,我創建了 hello.go
文件和 hello_test.go
單元測試代碼:
單元測試的目錄結構如下圖所示:
① hello.go
代碼:
package hello
import "rsc.io/quote"//引入第三方依賴模塊
func Hello() string {
return quote.Hello()
}
第 3 行代碼:需要導入第三方依賴模塊 rsc.io/quote
。
② hello_test.go
代碼:
package hello
import "testing"
func TestHello(t *testing.T) {
want := "Hello, world."
if got := Hello(); got != want {
t.Errorf("Hello() = %q, want %q", got, want)
}
}
1.打開Windows終端命令行,cd
到新項目的文件夾目錄。輸入命令:
go mod init XXX(你的文件夾名稱)
成功創建了 go.mod
文件,如下圖所示:
2.【重點!】從 Go 1.16 開始,創建完 go.mod
文件還必須執行指令:
$ go mod tidy
來增加項目需要的最小依賴。否則,運行 go test
指令時會報 no Go files in G:\hello
和 no required module provides package rsc.io/quote; to add it: go get rsc.io/quote
的錯誤。
運行結果如下圖所示,go mod 會自動拉取項目所需的最小依賴。
此時我們可以打開看看 go.mod
文件中的內容:
$ cat go.mod
輸出:
module hello
go 1.18
require rsc.io/quote v1.5.2
require (
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c // indirect
rsc.io/sampler v1.3.0 // indirect
)
在 Goland 中可以雙擊直接打開:
這樣,就成功編寫並測試了第一個模塊了。
2.4.4 go.mod文件結構
module
:指定模塊的名稱 (路徑)go
:依賴的原生 Go SDK 版本require
:項目所依賴的模塊replace
:可以替換依賴的模塊exclude
:可以忽略依賴的模塊
2.4.5 添加依賴
在完成上述所有操作後,發現 hello.go
文件還是編譯不通過,如下圖所示:
這時候我們再次執行 go test
指令,如下圖所示:
發現 go mod 會自動查找依賴並自動下載。單元測試通過。
【注意】此時我們已經開啓go mod模式了,但Goland可能仍出現 hello.go
文件的 import 報紅的情況。
【解決方法】如下圖設置,Environment
處填寫的 GOPROXY 網址要與cmd命令行輸入 go env
中的 GOPROXY 保持一致。設置好後重啓Goland即可。
go module 安裝 package 的原則是先拉最新的 release tag,若無tag則拉最新的commit,詳見 Modules官方介紹。 go 會自動生成一個 go.sum 文件來記錄 dependency tree:
- 在代碼沒有改變的情況下,再次執行代碼會發現跳過了檢查並安裝依賴的步驟。
- 可以使用命令
go list -m -u all
來檢查可以升級的 package ,使用go get -u need-upgrade-package
升級後會將新的依賴版本更新到 go.mod 中。也可以使用go get -u
升級所有依賴。
2.4.6 go get更新依賴
go get -u
:更新到最新的次要版本或者修訂版本(x.y.z)go get -u=patch
:更新到最新的修訂版本go get package@version
:更新到指定的版本號version- 運行
go get
如果有版本的更改,那麼 go.mod 文件也會作出相應的更改。