Go Modules 終極入門

Go modules 是 Go 語言中正式官宣的項目依賴解決方案,Go modules(前身為vgo)於 Go1.11 正式發佈,在 Go1.14 已經準備好,並且可以用在生產上(ready for production)了,Go官方也鼓勵所有用戶從其他依賴項管理工具遷移到 Go modules。

而 Go1.14,在近期也終於正式發佈,Go 官方親自 “喊” 你來用:

Go Modules 終極入門

因此在今天這篇文章中,我將給大家帶來 Go modules 的 “終極入門”,歡迎大家一起共同探討。

Go modules 是 Go 語言中正式官宣的項目依賴管理工具,Go modules(前身為vgo)於 Go1.11 正式發佈,在 Go1.14 已經準備好,並且可以用在生產上(ready for production)了,鼓勵所有用戶從其他依賴項管理工具遷移到 Go modules。

什麼是Go Modules

Go modules 是 Go 語言的依賴解決方案,發佈於 Go1.11,成長於 Go1.12,豐富於 Go1.13,正式於 Go1.14 推薦在生產上使用。

Go moudles 目前集成在 Go 的工具鏈中,只要安裝了 Go,自然而然也就可以使用 Go moudles 了,而 Go modules 的出現也解決了在 Go1.11 前的幾個常見爭議問題:

  1. Go 語言長久以來的依賴管理問題。
  2. “淘汰”現有的 GOPATH 的使用模式。
  3. 統一社區中的其它的依賴管理工具(提供遷移功能)。

GOPATH的那些點點滴滴

我們有提到 Go modules 的解決的問題之一就是“淘汰”掉 GOPATH,但是 GOPATH 又是什麼呢,為什麼在 Go1.11 前就使用 GOPATH,而 Go1.11 後就開始逐步建議使用 Go modules,不再推薦 GOPATH 的模式了呢?

GOPATH是什麼

我們先看看第一個問題,GOPATH 是什麼,我們可以輸入如下命令查看:

<code>$ go env
GOPATH="/Users/eddycjy/go"
.../<code>

我們輸入 go env 命令行後可以查看到 GOPATH 變量的結果,我們進入到該目錄下進行查看,如下:

<code>go
├── bin
├── pkg
└── src
├── github.com
├── golang.org
├── google.golang.org
├── gopkg.in
..../<code>

GOPATH目錄下一共包含了三個子目錄,分別是:

  • bin:存儲所編譯生成的二進制文件。
  • pkg:存儲預編譯的目標文件,以加快程序的後續編譯速度。
  • src:存儲所有 .go 文件或源代碼。在編寫 Go 應用程序,程序包和庫時,一般會以 $GOPATH/src/github.com/foo/bar 的路徑進行存放。

因此在使用 GOPATH 模式下,我們需要將應用代碼存放在固定的 $GOPATH/src 目錄下,並且如果執行 go get 來拉取外部依賴會自動下載並安裝到 $GOPATH 目錄下。

為什麼棄用GOPATH模式

在 GOPATH 的 $GOPATH/src 下進行 .go 文件或源代碼的存儲,我們可以稱其為 GOPATH 的模式,這個模式,看起來好像沒有什麼問題,那麼為什麼我們要棄用呢,參見如下原因:

  • GOPATH 模式下沒有版本控制的概念,具有致命的缺陷,至少會造成以下問題:go get github.com/foo/bar
  • Go 語言官方從 Go1.11 起開始推進 Go modules(前身vgo),Go1.13 起不再推薦使用 GOPATH 的使用模式,Go modules 也漸趨穩定,因此新項目也沒有必要繼續使用GOPATH模式。

在GOPATH模式下的產物

Go1 在 2012 年 03 月 28 日發佈,而 Go1.11 是在 2018 年 08 月 25 日才正式發佈(數據來源:Github Tag),在這個空檔的時間內,並沒有 Go modules 這一個東西,最早期可能還好說,因為剛發佈,用的人不多,所以沒有明顯暴露,但是後期 Go 語言使用的人越來越多了,那怎麼辦?

這時候社區中逐漸的湧現出了大量的依賴解決方案,百花齊放,讓人難以挑選,其中包括我們所熟知的 vendor 目錄的模式,以及曾經一度被認為是“官宣”的 dep 的這類依賴管理工具。

但為什麼 dep 沒有正在成為官宣呢,其實是因為隨著 Russ Cox 與 Go 團隊中的其他成員不斷深入地討論,發現dep 的一些細節似乎越來越不適合 Go,因此官方採取了另起 proposal 的方式來推進,其方案的結果一開始先是釋出 vgo(Go modules的前身,知道即可,不需要深入瞭解),最終演變為我們現在所見到的 Go modules,也在 Go1.11 正式進入了 Go 的工具鏈。

因此與其說是 “在GOPATH模式下的產物”,不如說是歷史為當前提供了重要的教訓,因此出現了 Go modules。

Go Modules基本使用

在初步瞭解了 Go modules 的前世今生後,我們正式進入到 Go modules 的使用,首先我們將從頭開始創建一個 Go modules 的項目(原則上所創建的目錄應該不要放在 GOPATH 之中)。

所提供的命令

在 Go modules 中,我們能夠使用如下命令進行操作:

命令作用go mod init生成 go.mod 文件go mod download下載 go.mod 文件中指明的所有依賴go mod tidy整理現有的依賴go mod graph查看現有的依賴結構go mod edit編輯 go.mod 文件go mod vendor導出項目所有的依賴到vendor目錄go mod verify校驗一個模塊是否被篡改過go mod why查看為什麼需要依賴某模塊

所提供的環境變量

在 Go modules 中有如下常用環境變量,我們可以通過 go env 命令來進行查看,如下:

<code>$ go env
GO111MODULE="auto"
GOPROXY="https://proxy.golang.org,direct"
GONOPROXY=""
GOSUMDB="sum.golang.org"
GONOSUMDB=""
GOPRIVATE=""
.../<code>

GO111MODULE

Go語言提供了 GO111MODULE 這個環境變量來作為 Go modules 的開關,其允許設置以下參數:

  • auto:只要項目包含了 go.mod 文件的話啟用 Go modules,目前在 Go1.11 至 Go1.14 中仍然是默認值。
  • on:啟用 Go modules,推薦設置,將會是未來版本中的默認值。
  • off:禁用 Go modules,不推薦設置。

GO111MODULE的小歷史

你可能會留意到 GO111MODULE 這個名字比較“奇特”,實際上在 Go 語言中經常會有這類階段性的變量, GO111MODULE 這個命名代表著Go語言在 1.11 版本添加的,針對 Module 的變量。

像是在 Go1.5 版本的時候,也發佈了一個系統環境變量 GO15VENDOREXPERIMENT,作用是用於開啟 vendor 目錄的支持,當時其默認值也不是開啟,僅僅作為 experimental。其隨後在 Go1.6 版本時也將默認值改為了開啟,並且最後作為了official,GO15VENDOREXPERIMENT 系統變量就退出了歷史舞臺。

而未來 GO111MODULE 這一個系統環境變量也會面臨這個問題,也會先調整為默認值為 on(曾經在Go1.13想想改為 on,並且已經合併了 PR,但最後因為種種原因改回了 auto),然後再把 GO111MODULE 的支持給去掉,我們猜測應該會在 Go2 將 GO111MODULE 給去掉,因為如果直接去掉 GO111MODULE 的支持,會存在兼容性問題。

GOPROXY

這個環境變量主要是用於設置 Go 模塊代理(Go module proxy),其作用是用於使 Go 在後續拉取模塊版本時能夠脫離傳統的 VCS 方式,直接通過鏡像站點來快速拉取。

GOPROXY 的默認值是: https://proxy.golang.org,direct ,這有一個很嚴重的問題,就是 proxy.golang.org 在國內是無法訪問的,因此這會直接卡住你的第一步,所以你必須在開啟 Go modules 的時,同時設置國內的 Go 模塊代理,執行如下命令:

<code>$ go env -w GOPROXY=https://goproxy.cn,direct/<code>

GOPROXY的值是一個以英文逗號 “,” 分割的 Go 模塊代理列表,允許設置多個模塊代理,假設你不想使用,也可以將其設置為 “off” ,這將會禁止 Go 在後續操作中使用任何 Go 模塊代理。

direct是什麼

而在剛剛設置的值中,我們可以發現值列表中有 “direct” 標識,它又有什麼作用呢?

實際上 “direct” 是一個特殊指示符,用於指示 Go 回源到模塊版本的源地址去抓取(比如 GitHub 等),場景如下:當值列表中上一個 Go 模塊代理返回 404 或 410 錯誤時,Go 自動嘗試列表中的下一個,遇見 “direct” 時回源,也就是回到源地址去抓取,而遇見 EOF 時終止並拋出類似 “invalid version: unknown revision...” 的錯誤。

GOSUMDB

它的值是一個 Go checksum database,用於在拉取模塊版本時(無論是從源站拉取還是通過 Go module proxy 拉取)保證拉取到的模塊版本數據未經過篡改,若發現不一致,也就是可能存在篡改,將會立即中止。

GOSUMDB的默認值為: sum.golang.org ,在國內也是無法訪問的,但是 GOSUMDB 可以被 Go 模塊代理所代理(詳見:Proxying a Checksum Database)。

因此我們可以通過設置 GOPROXY 來解決,而先前我們所設置的模塊代理 goproxy.cn 就能支持代理 sum.golang.org ,所以這一個問題在設置 GOPROXY 後,你可以不需要過度關心。

另外若對 GOSUMDB 的值有自定義需求,其支持如下格式:

<code><sumdb>+<public>
<sumdb>+<public> <sumdb>
/<sumdb>/<public>/<sumdb>/<public>/<sumdb>/<code>

也可以將其設置為“off”,也就是禁止 Go 在後續操作中校驗模塊版本。

GONOPROXY/GONOSUMDB/GOPRIVATE

這三個環境變量都是用在當前項目依賴了私有模塊,例如像是你公司的私有 git 倉庫,又或是 github 中的私有庫,都是屬於私有模塊,都是要進行設置的,否則會拉取失敗。

更細緻來講,就是依賴了由 GOPROXY 指定的 Go 模塊代理或由 GOSUMDB 指定 Go checksum database 都無法訪問到的模塊時的場景。

而一般建議直接設置 GOPRIVATE,它的值將作為 GONOPROXY 和 GONOSUMDB 的默認值,所以建議的最佳姿勢是直接使用 GOPRIVATE。

並且它們的值都是一個以英文逗號 “,” 分割的模塊路徑前綴,也就是可以設置多個,例如:

<code>$ go env -w GOPRIVATE="git.example.com,github.com/eddycjy/mquote"/<code>

設置後,前綴為 git.xxx.com 和 github.com/eddycjy/mquote 的模塊都會被認為是私有模塊。

如果不想每次都重新設置,我們也可以利用通配符,例如:

<code>$ go env -w GOPRIVATE="*.example.com"/<code>

這樣子設置的話,所有模塊路徑為 example.com 的子域名(例如:git.example.com)都將不經過 Go module proxy 和 Go checksum database,需要注意的是不包括 example.com 本身。

開啟Go Modules

目前Go modules並不是默認開啟,因此Go語言提供了GO111MODULE這個環境變量來作為Go modules的開關,其允許設置以下參數:

  • auto:只要項目包含了go.mod文件的話啟用 Go modules,目前在Go1.11至Go1.14中仍然是默認值。
  • on:啟用 Go modules,推薦設置,將會是未來版本中的默認值。
  • off:禁用 Go modules,不推薦設置。

如果你不確定你當前的值是什麼,可以執行 go env 命令,查看結果:

<code>$ go env
GO111MODULE="off"
...
/<code>

如果需要對GO111MODULE的值進行變更,推薦通過 go env 命令進行設置:

<code>$ go env -w GO111MODULE=on/<code>

但是需要注意的是如果對應的系統環境變量有值了(進行過設置), go env 是不支持覆蓋寫入的,否則會出現如下報錯信息: warning: go env -w GO111MODULE=... does not override conflicting OS environment variable 。

又或是可以通過直接設置系統環境變量(寫入對應的.bash_profile文件亦可)來實現這個目的:

<code>$ export GO111MODULE=on/<code>

初始化項目

在完成 Go modules 的開啟後,我們需要創建一個示例項目來進行演示,執行如下命令:

<code>$ mkdir -p $HOME/eddycjy/module-repo 
$ cd $HOME/eddycjy/module-repo
/<code>

然後進行Go modules的初始化,如下:

<code>$ go mod init github.com/eddycjy/module-repo
go: creating new go.mod: module github.com/eddycjy/module-repo/<code>

在執行 go mod init 命令時,我們指定了模塊導入路徑為 github.com/eddycjy/module-repo 。接下來我們在該項目根目錄下創建 main.go 文件,如下:

<code>package main

import (
"fmt"
"github.com/eddycjy/mquote"
)

func main() {
\tfmt.Println(mquote.GetHello())
}/<code>

然後在項目根目錄執行 go get github.com/eddycjy/mquote 命令,如下:

<code>$ go get github.com/eddycjy/mquote 
go: finding github.com/eddycjy/mquote latest
go: downloading github.com/eddycjy/mquote v0.0.0-20200220041913-e066a990ce6f
go: extracting github.com/eddycjy/mquote v0.0.0-20200220041913-e066a990ce6f/<code>

查看go.mod 文件

在初始化項目時,會生成一個 go.mod 文件,是啟用了 Go modules 項目所必須的最重要的標識,同時也是GO111MODULE 值為 auto 時的識別標識,它描述了當前項目(也就是當前模塊)的元信息,每一行都以一個動詞開頭。

在我們剛剛進行了初始化和簡單拉取後,我們再次查看go.mod文件,基本內容如下:

<code>module github.com/eddycjy/module-repo

go 1.13

require (
\tgithub.com/eddycjy/mquote v0.0.0-20200220041913-e066a990ce6f
)/<code>

為了更進一步的講解,我們模擬引用如下:

<code>module github.com/eddycjy/module-repo

go 1.13

require (
example.com/apple v0.1.2
example.com/banana v1.2.3
example.com/banana/v2 v2.3.4
example.com/pear // indirect
example.com/strawberry // incompatible
)

exclude example.com/banana v1.2.4
replace example.com/apple v0.1.2 => example.com/fried v0.1.0
replace example.com/banana => example.com/fish/<code>
  • module:用於定義當前項目的模塊路徑。
  • go:用於標識當前模塊的 Go 語言版本,值為初始化模塊時的版本,目前來看還只是個標識作用。
  • require:用於設置一個特定的模塊版本。
  • exclude:用於從使用中排除一個特定的模塊版本。
  • replace:用於將一個模塊版本替換為另外一個模塊版本。

另外你會發現 example.com/pear 的後面會有一個 indirect 標識,indirect 標識表示該模塊為間接依賴,也就是在當前應用程序中的 import 語句中,並沒有發現這個模塊的明確引用,有可能是你先手動 go get 拉取下來的,也有可能是你所依賴的模塊所依賴的,情況有好幾種。

查看go.sum文件

在第一次拉取模塊依賴後,會發現多出了一個 go.sum 文件,其詳細羅列了當前項目直接或間接依賴的所有模塊版本,並寫明瞭那些模塊版本的 SHA-256 哈希值以備 Go 在今後的操作中保證項目所依賴的那些模塊版本不會被篡改。

<code>github.com/eddycjy/mquote v0.0.1 h1:4QHXKo7J8a6J/k8UA6CiHhswJQs0sm2foAQQUq8GFHM=
github.com/eddycjy/mquote v0.0.1/go.mod h1:ZtlkDs7Mriynl7wsDQ4cU23okEtVYqHwl7F1eDh4qPg=
github.com/eddycjy/mquote/module/tour v0.0.1 h1:cc+pgV0LnR8Fhou0zNHughT7IbSnLvfUZ+X3fvshrv8=
github.com/eddycjy/mquote/module/tour v0.0.1/go.mod h1:8uL1FOiQJZ4/1hzqQ5mv4Sm7nJcwYu41F3nZmkiWx5I=
.../<code>

我們可以看到一個模塊路徑可能有如下兩種:

<code>github.com/eddycjy/mquote v0.0.1 h1:4QHXKo7J8a6J/k8UA6CiHhswJQs0sm2foAQQUq8GFHM=
github.com/eddycjy/mquote v0.0.1/go.mod h1:ZtlkDs7Mriynl7wsDQ4cU23okEtVYqHwl7F1eDh4qPg=/<code>

h1 hash 是 Go modules 將目標模塊版本的 zip 文件開包後,針對所有包內文件依次進行 hash,然後再把它們的 hash 結果按照固定格式和算法組成總的 hash 值。

而 h1 hash 和 go.mod hash 兩者,要不就是同時存在,要不就是隻存在 go.mod hash。那什麼情況下會不存在 h1 hash 呢,就是當 Go 認為肯定用不到某個模塊版本的時候就會省略它的 h1 hash,就會出現不存在 h1 hash,只存在 go.mod hash 的情況。

查看全局緩存

我們剛剛成功的將 github.com/eddycjy/mquote 模塊拉取了下來,其拉取的結果緩存在 $GOPATH/pkg/mod 和 $GOPATH/pkg/sumdb 目錄下,而在 mod 目錄下會以 github.com/foo/bar 的格式進行存放,如下:

<code>mod
├── cache
├── github.com
├── golang.org
├── google.golang.org
├── gopkg.in
.../<code>

需要注意的是同一個模塊版本的數據只緩存一份,所有其它模塊共享使用。如果你希望清理所有已緩存的模塊版本數據,可以執行 go clean -modcache 命令。

Go Modules下的go get行為

在拉取項目依賴時,你會發現拉取的過程總共分為了三大步,分別是 finding(發現)、downloading(下載)以及 extracting(提取), 並且在拉取信息上一共分為了三段內容:

Go Modules 終極入門

需要注意的是,所拉取版本的 commit 時間是以UTC時區為準,而並非本地時區,同時我們會發現我們 go get 命令所拉取到的版本是 v0.0.0,這是因為我們是直接執行 go get -u 獲取的,並沒有指定任何的版本信息,由 Go modules 自行按照內部規則進行選擇。

go get的拉取行為

剛剛我們用 go get 命令拉取了新的依賴,那麼 go get 又提供了哪些功能呢,常用的拉取命令如下:

命令作用go get拉取依賴,會進行指定性拉取(更新),並不會更新所依賴的其它模塊。go get -u更新現有的依賴,會強制更新它所依賴的其它全部模塊,不包括自身。go get -u -t ./...更新所有直接依賴和間接依賴的模塊版本,包括單元測試中用到的。

那麼我想選擇具體版本應當如何執行呢,如下:

命令作用go get golang.org/x/text@latest拉取最新的版本,若存在tag,則優先使用。go get golang.org/x/text@master拉取 master 分支的最新 commit。go get golang.org/x/[email protected]拉取 tag 為 v0.3.2 的 commit。go get golang.org/x/text@342b2e拉取 hash 為 342b231 的 commit,最終會被轉換為 v0.3.2。

go get的版本選擇

我們回顧一下我們拉取的 go get github.com/eddycjy/mquote ,其結果是 v0.0.0-20200220041913-e066a990ce6f ,對照著上面所提到的 go get 行為來看,你可能還會有一些疑惑,那就是在 go get 沒有指定任何版本的情況下,它的版本選擇規則是怎麼樣的,也就是為什麼 go get 拉取的是 v0.0.0 ,它什麼時候會拉取正常帶版本號的 tags 呢。實際上這需要區分兩種情況,如下:

  1. 所拉取的模塊有發佈 tags:如果只有單個模塊,那麼就取主版本號最大的那個tag。如果有多個模塊,則推算相應的模塊路徑,取主版本號最大的那個tag(子模塊的tag的模塊路徑會有前綴要求)
  2. 所拉取的模塊沒有發佈過 tags:默認取主分支最新一次 commit 的 commithash。

沒有發佈過 tags

那麼為什麼會拉取的是 v0.0.0 呢,是因為 github.com/eddycjy/mquote 沒有發佈任何的tag,如下:

Go Modules 終極入門

因此它默認取的是主分支最新一次 commit 的 commit 時間和 commithash,也就是 20200220041913-e066a990ce6f ,屬於第二種情況。

有發佈 tags

在項目有發佈 tags 的情況下,還存在著多種模式,也就是隻有單個模塊和多個模塊,我們統一以多個模塊來進行展示,因為多個模塊的情況下就已經包含了單個模塊的使用了,如下圖:

Go Modules 終極入門

在這個項目中,我們一共打了兩個tag,分別是:v0.0.1 和 module/tour/v0.0.1。這時候你可能會奇怪,為什麼要打 module/tour/v0.0.1 這麼“奇怪”的tag,這有什麼用意嗎?

其實是 Go modules 在同一個項目下多個模塊的tag表現方式,其主要目錄結構為:

<code>mquote
├── go.mod
├── module
│ └── tour
│ ├── go.mod
│ └── tour.go
└── quote.go/<code>

可以看到在 mquote 這個項目的根目錄有一個 go.mod 文件,而在 module/tour 目錄下也有一個 go.mod 文件,其模塊導入和版本信息的對應關係如下:

tag模塊導入路徑含義v0.0.1github.com/eddycjy/mquotemquote 項目的v 0.0.1 版本module/tour/v0.01github.com/eddycjy/mquote/module/tourmquote 項目下的子模塊 module/tour 的 v0.0.1 版本

導入主模塊和子模塊

結合上述內容,拉取主模塊的話,還是照舊執行如下命令:

<code>$ go get github.com/eddycjy/[email protected]
go: finding github.com/eddycjy/mquote v0.0.1
go: downloading github.com/eddycjy/mquote v0.0.1
go: extracting github.com/eddycjy/mquote v0.0.1/<code>

如果是想拉取子模塊,執行如下命令:

<code>$ go get github.com/eddycjy/mquote/module/[email protected]
go: finding github.com/eddycjy/mquote/module v0.0.1
go: finding github.com/eddycjy/mquote/module/tour v0.0.1
go: downloading github.com/eddycjy/mquote/module/tour v0.0.1
go: extracting github.com/eddycjy/mquote/module/tour v0.0.1/<code>

我們將主模塊和子模塊的拉取進行對比,你會發現子模塊的拉取會多出一步,它會先發現 github.com/eddycjy/mquote/module ,再繼續推算,最終拉取到 module/tour 。

Go Modules的導入路徑說明

不同版本的導入路徑

在前面的模塊拉取和引用中,你會發現我們的模塊導入路徑就是 github.com/eddycjy/mquote 和 github.com/eddycjy/mquote/module/tour ,似乎並沒有什麼特殊的。

其實不然,實際上 Go modules 在主版本號為 v0 和 v1 的情況下省略了版本號,而在主版本號為v2及以上則需要明確指定出主版本號,否則會出現衝突,其tag與模塊導入路徑的大致對應關係如下:

tag模塊導入路徑v0.0.0github.com/eddycjy/mquotev1.0.0github.com/eddycjy/mquotev2.0.0github.com/eddycjy/mquote/v2v3.0.0github.com/eddycjy/mquote/v3

簡單來講,就是主版本號為 v0 和 v1 時,不需要在模塊導入路徑包含主版本的信息,而在 v1 版本以後,也就是 v2 起,必須要在模塊的導入路徑末尾加上主版本號,引用時就需要調整為如下格式:

<code>import (
"github.com/eddycjy/mquote/v2/example"
)/<code>

另外忽略主版本號 v0 和 v1 是強制性的(不是可選項),因此每個軟件包只有一個明確且規範的導入路徑。

為什麼忽略v0和v1的主版本號

  1. 導入路徑中忽略 v1 版本的原因是:考慮到許多開發人員創建一旦到達 v1 版本便永不改變的軟件包,這是官方所鼓勵的,不認為所有這些開發人員在無意發佈 v2 版時都應被迫擁有明確的 v1 版本尾綴,這將導致 v1 版本變成“噪音”且無意義。
  2. 導入路徑中忽略了 v0 版本的原因是:根據語義化版本規範,v0的這些版本完全沒有兼容性保證。需要一個顯式的 v0 版本的標識對確保兼容性沒有多大幫助。

Go Modules的語義化版本控制

我們不斷地在 Go Modules 的使用中提到版本號,其實質上被稱為“語義化版本”,假設我們的版本號是 v1.2.3,如下:

Go Modules 終極入門

其版本格式為“主版本號.次版本號.修訂號”,版本號的遞增規則如下:

  1. 主版本號:當你做了不兼容的 API 修改。
  2. 次版本號:當你做了向下兼容的功能性新增。
  3. 修訂號:當你做了向下兼容的問題修正。

假設你是先行版本號或特殊情況,可以將版本信息追加到“主版本號.次版本號.修訂號”的後面,作為延伸,如下:

Go Modules 終極入門

至此我們介紹了 Go modules 所支持的兩類版本號方式,在我們發佈新版本打 tag 的時候,需要注意遵循,否則不遵循語義化版本規則的版本號都是無法進行拉取的。

Go Modules的最小版本選擇

現在我們已經有一個模塊,也有發佈的 tag,但是一個模塊往往依賴著許多其它許許多多的模塊,並且不同的模塊在依賴時很有可能會出現依賴同一個模塊的不同版本,如下圖(來自Russ Cox):

Go Modules 終極入門

在上述依賴中,模塊 A 依賴了模塊 B 和模塊 C,而模塊 B 依賴了模塊 D,模塊 C 依賴了模塊 D 和 F,模塊 D 又依賴了模塊 E,而且同模塊的不同版本還依賴了對應模塊的不同版本。那麼這個時候 Go modules 怎麼選擇版本,選擇的是哪一個版本呢?

我們根據 proposal 可得知,Go modules 會把每個模塊的依賴版本清單都整理出來,最終得到一個構建清單,如下圖(來自Russ Cox):

Go Modules 終極入門

我們看到 rough list 和 final list,兩者的區別在於重複引用的模塊 D(v1.3、v1.4),其最終清單選用了模塊 D 的 v1.4 版本,主要原因:

  1. 語義化版本的控制:因為模塊 D 的 v1.3 和 v1.4 版本變更,都屬於次版本號的變更,而在語義化版本的約束下,v1.4 必須是要向下兼容 v1.3 版本,因此認為不存在破壞性變更,也就是兼容的。
  2. 模塊導入路徑的規範:主版本號不同,模塊的導入路徑不一樣,因此若出現不兼容的情況,其主版本號會改變,模塊的導入路徑自然也就改變了,因此不會與第一點的基礎相沖突。

go.sum文件要不要提交

理論上 go.mod 和 go.sum 文件都應該提交到你的 Git 倉庫中去。

假設我們不上傳 go.sum 文件,就會造成每個人執行 Go modules 相關命令,又會生成新的一份 go.sum,也就是會重新到上游拉取,再拉取時有可能就是被篡改過的了,會有很大的安全隱患,失去了與基準版本(第一個所提交的人,所期望的版本)的校驗內容,因此 go.sum文件是需要提交。

總結

至此我們介紹了 Go modules 的前世今生、基本使用和在 Go modules 模式下 go get 命令的行為轉換,同時我們對常見的多版本導入路徑、語義化版本控制以及多模塊的最小版本選擇規則進行了大致的介紹。

Go modules 的成長和發展經歷了一定的過程,如果你是剛接觸的讀者,直接基於 Go modules 的項目開始即可,如果既有老項目,那麼是時候考慮切換過來了,Go1.14起已經準備就緒,並推薦你使用。


分享到:


相關文章: