Go Modules 終極入門

EDDYCJY發表於2020-02-28

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

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

image

因此在今天這篇文章中,我將給大家帶來 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 是什麼,我們可以輸入如下命令檢視:

$ go env
GOPATH="/Users/eddycjy/go"
...

我們輸入go env命令列後可以檢視到 GOPATH 變數的結果,我們進入到該目錄下進行檢視,如下:

go
├── bin
├── pkg
└── src
    ├── github.com
    ├── golang.org
    ├── google.golang.org
    ├── gopkg.in
    ....

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的時候,你無法傳達任何的版本資訊的期望,也就是說你也無法知道自己當前更新的是哪一個版本,也無法通過指定來拉取自己所期望的具體版本。
    • 在執行 Go 應用程式的時候,你無法保證其它人與你所期望依賴的第三方庫是相同的版本,也就是說在專案依賴庫的管理上,你無法保證所有人的依賴版本都一致。
    • 你沒辦法處理 v1、v2、v3 等等不同版本的引用問題,因為 GOPATH 模式下的匯入路徑都是一樣的,都是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 命令來進行檢視,如下:

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

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 模組代理,執行如下命令:

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

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 的值有自定義需求,其支援如下格式:

  • 格式 1:<SUMDB_NAME>+<PUBLIC_KEY>
  • 格式 2:<SUMDB_NAME>+<PUBLIC_KEY> <SUMDB_URL>

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

GONOPROXY/GONOSUMDB/GOPRIVATE

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

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

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

並且它們的值都是一個以英文逗號 “,” 分割的模組路徑字首,也就是可以設定多個,例如:

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

設定後,字首為 git.xxx.com 和 github.com/eddycjy/mquote 的模組都會被認為是私有模組。

如果不想每次都重新設定,我們也可以利用萬用字元,例如:

$ go env -w GOPRIVATE="*.example.com"

這樣子設定的話,所有模組路徑為 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命令,檢視結果:

$ go env
GO111MODULE="off"
...

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

$ go env -w GO111MODULE=on

但是需要注意的是如果對應的系統環境變數有值了(進行過設定),會出現如下警告資訊:warning: go env -w GO111MODULE=... does not override conflicting OS environment variable

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

$ export GO111MODULE=on

初始化專案

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

$ mkdir -p $HOME/eddycjy/module-repo 
$ cd $HOME/eddycjy/module-repo

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

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

在執行 go mod init 命令時,我們指定了模組匯入路徑為 github.com/eddycjy/module-repo。接下來我們在該專案根目錄下建立 main.go 檔案,如下:

package main

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

func main() {
    fmt.Println(mquote.GetHello())
}

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

$ 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

檢視 go.mod 檔案

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

在我們剛剛進行了初始化和簡單拉取後,我們再次檢視 go.mod 檔案,基本內容如下:

module github.com/eddycjy/module-repo

go 1.13

require (
    github.com/eddycjy/mquote v0.0.0-20200220041913-e066a990ce6f
)

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

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
  • module:用於定義當前專案的模組路徑。
  • go:用於標識當前模組的 Go 語言版本,值為初始化模組時的版本,目前來看還只是個標識作用。
  • require:用於設定一個特定的模組版本。
  • exclude:用於從使用中排除一個特定的模組版本。
  • replace:用於將一個模組版本替換為另外一個模組版本。

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

檢視 go.sum 檔案

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

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=
...

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

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

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 的格式進行存放,如下:

mod
├── cache
├── github.com
├── golang.org
├── google.golang.org
├── gopkg.in
...

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

Go Modules 下的 go get 行為

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

image

需要注意的是,所拉取版本的 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/text@v0.3.2 拉取 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,如下:

image

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

有釋出 tags

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

image

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

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

mquote
├── go.mod
├── module
│   └── tour
│       ├── go.mod
│       └── tour.go
└── quote.go

可以看到在 mquote 這個專案的根目錄有一個 go.mod 檔案,而在 module/tour 目錄下也有一個 go.mod 檔案,其模組匯入和版本資訊的對應關係如下:

tag 模組匯入路徑 含義
v0.0.1 github.com/eddycjy/mquote mquote 專案的 v 0.0.1 版本
module/tour/v0.01 github.com/eddycjy/mquote/module/tour mquote 專案下的子模組 module/tour 的 v0.0.1 版本

匯入主模組和子模組

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

$ go get github.com/eddycjy/mquote@v0.0.1
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

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

$ go get github.com/eddycjy/mquote/module/tour@v0.0.1
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

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

Go Modules 的匯入路徑說明

不同版本的匯入路徑

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

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

tag 模組匯入路徑
v0.0.0 github.com/eddycjy/mquote
v1.0.0 github.com/eddycjy/mquote
v2.0.0 github.com/eddycjy/mquote/v2
v3.0.0 github.com/eddycjy/mquote/v3

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

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

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

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

  1. 匯入路徑中忽略 v1 版本的原因是:考慮到許多開發人員建立一旦到達 v1 版本便永不改變的軟體包,這是官方所鼓勵的,不認為所有這些開發人員在無意釋出 v2 版時都應被迫擁有明確的 v1 版本尾綴,這將導致 v1 版本變成 “噪音” 且無意義。

  2. 匯入路徑中忽略了 v0 版本的原因是:根據語義化版本規範,v0 的這些版本完全沒有相容性保證。需要一個顯式的 v0 版本的標識對確保相容性沒有多大幫助。

Go Modules 的語義化版本控制

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

image

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

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

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

image

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

Go Modules 的最小版本選擇

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

image

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

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

image

我們看到 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 起已經準備就緒,並推薦你使用。

我的公眾號


image

參考

更多原創文章乾貨分享,請關注公眾號
  • Go Modules 終極入門
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章