什么是 module?module 解決了什么問題?
module 代表一個版本管理單元,它包括一個或者多個 packages。
一般來說,一個版本控制倉庫(比如 golang.org/x/text
)包含一個 module(也可以包含多個 module,但是通常會帶來一些復雜性)。
module 在 Go1.11 版本發布,它的前身是 vgo。 在 Go1.9.7+ 版本和 1.10.3+ 版本做了對 module 的部分向后兼容。
module 機制會在項目的根目錄中添加 go.mod, 該文件用來記錄項目依賴的 modules 的版本。
module 的出現主要是為了解決以下問題:
1. 版本依賴管理
設想一下,如果有 3 個包, 分別為 foo1, foo2, foo3。
foo1 依賴 foo3 的版本 v1.0.1 (后續簡寫為 foo3@v1.0.1), foo2 依賴 foo3@v1.0.2。
現在我們需要實現一個功能,需要同時使用 foo1 和 foo2 兩個包, 那我們應該使用什么版本的 foo3 呢?
2. 解除對 GOPATH 的依賴
在 Go1.11 版本之前,所有的 go 代碼都要放到 $GOPATH/src 目錄下面, 以便 import 能找到對應的包。
而 module 的出現,可以讓我們將 go 代碼放到任何地方。
語義導入版本控制
語義導入版本控制 (Semantic Import Versioning),是使用 module 必須要遵循的一些規定。
簡單說來,就是需要 modules 的不同版本滿足一些兼容規則。 比如: v1.5.4 版本需要向前兼容 v1.5.0、v1.4.0 甚至 v1.0.0 版本, 但不用兼容 v0.0.9 版本。
另外語義導入版本控制還約定了版本不能向前兼容時,modules 下的包的導入路徑的變化。
下面詳細介紹具體要滿足哪些規則, 以及 golang 工具鏈是如何選擇版本的:
1. semver 規范
semver 是一個語義化版本規范,是 modules 需要遵從的。
sember 的版本格式為:主版本號.次版本號.修訂號,版本號遞增規則如下:
- 主版本號:當你做了不兼容的 API 修改
- 次版本號:當你做了向下兼容的功能性新增,
- 修訂號:當你做了向下兼容的問題修正。
例如: 現在最新的版本號如果是 v1.4.9。 在此基礎上,
- 如果要對接口作出參數或返回值調整,導致依賴這個項目的代碼需要修改它們的代碼。那么下一個版本號應該是 v2.0.0
- 如果是增加新的功能,不影響舊接口。那么下一個版本號應該是 v1.5.0
- 如果是修改了一些 bug,而且可以向前兼容。那么下一個版本號應該是 v1.4.10
具體規則可以參考 https://semver.org/
2. Go 官方的 導入兼容規則
如果新 package 和舊 package 擁有相同的導入路徑, 那么新的 package 要兼容舊的 package。
舉個例子,比如你開發了一個 module (github.com/you/foo) 提供給用戶使用,最初的時候你給這個 module 打了一個版本為 v1.0.0。並且直到 v1.5.9 為止沒有出現過不能向前兼容的情況。
但現在,你要發布一個全新的版本,從而不能向前兼容。所以 semver 規則,你需要將版本號定義成 v2.0.0。
然而, 導入兼容規則 又給你加了一個新的限制,你的新版本不能向老版本兼容,所以你必須修改包路徑為 github.com/you/foo/v2 (后文會詳細介紹怎么修改包路徑)。
3. 版本選擇算法
在介紹版本選擇算法之前, 讓我們先了解一下 module 是怎么存儲版本信息的:
如果你在自己的 module 中 import 了一個公共 moduel (github.com/other/bar),那么你第一次執行 go build
或者 go test
的時候,go 會幫你自動找出並且下載 github.com/other/bar 的最新版本。並且在 go.mod 中記錄當前依賴的版本, 如 require github.com/other/bar v1.4.9
。 如果你事先手動在 go.mod 中增加了 require github.com/other/bar v1.4.8
, 那么此時你執行 go build
或者 go test
時, go 會使用 v1.4.8 版本的 module 來編譯。
那版本選擇算法是什么呢?讓我們先回到之前提出的那個問題:
“ 如果有 3 個包, 分別為 foo1, foo2, foo3。 foo1 依賴 foo3@v1.0.1, foo2 依賴 foo3@v1.0.2。 現在我們需要實現一個功能,需要同時使用 foo1 和 foo2 兩個包, 那我們應該使用什么版本的 foo3 呢?”
這里我們假設 foo1,foo2,foo3 都使用了 module,並且我們實現的這個功能也使用了 module (假設我們的 module 名字叫做 bar )
對於這種情況,在 foo1 的根目錄下, 有一個 go.mod 文件, 包括一行依賴信息; require foo3 v1.0.1
。 在 foo2 的根目錄下, 有一個 go.mod 文件, 包括一行依賴信息; require foo3 v1.0.2
。
那么在編譯我們自己的 module bar 時, 會使用哪個版本的 foo3 呢? 答案是 v1.0.2。
將 golang 選擇 foo3 的版本的算法叫做 最小版本選擇算法:
它選出來的版本是所有 go.mod 文件(在這里包括 foo1, foo2 和 bar 下的 go.mod 文件) 中明確指定的最大版本。
這里的最小的意思是 foo1 和 foo2 給出的依賴的版本都是最小化了的, 比如 foo1 依賴 foo3@v1.0.1, 那么根據 semver 規則, foo1 在 foo3@v1.0.2 下也可以正常工作, 因為 foo3@v1.0.2 是向前兼容了 foo3@v1.0.1 的。
那么如果 foo2 依賴的是 foo3@v2.1.1, 我們編譯 bar 時,會使用哪個版本的 foo3 呢? 答案是:v1.0.1 和 v2.1.1 。
注意: 根據 導入兼容規則, v1.0.1 和 v2.1.1 使用的是不同的路徑,一個是 v1.0.1 使用的是 foo3,而 v2.1.1 使用的是 foo3/v2。 所以可以同時存在於一次編譯中。 而且 v2.1.1 是不能兼容 v1.0.1 的,所以 foo1 沒法使用 v2.1.1 版本,因此也必須同時使用 foo3 的兩個版本。
關於 最小版本選擇算法 的詳細信息,參考: https://research.swtch.com/vgo-mvs
4. “偽”版本
如果一個 module 沒有有效的 semver 版本,那么 go.mod 將通過一個叫做 “偽版本“ 的東西來記錄版本。
”偽版本“ 的通常形式是 vX.0.0-yyyymmddhhmmss-abcdefabcdef。 比如 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
其中 v0.0.0 表示 semver 版本號, 20170915032832 表示這個版本的時間。 14c0d48ead0c 表示這次提交的 hash。
怎么使用?中國用戶會遇到哪些問題?如何解決這些問題?
這一節主要介紹怎么使用 go module,以及牆內用戶怎么解決牆外的下載問題。
先看一下官方給的一個例子:
# 在 $GOPATH 外部創建一個目錄
$ mkdir -p /tmp/scratchpad/hello
$ cd /tmp/scratchpad/hello
# 初始化 module
$ go mod init github.com/you/hello
go: creating new go.mod: module github.com/you/hello
# 依賴 module 寫一段代碼
$ cat <<EOF > hello.go
package main
import (
"fmt"
"rsc.io/quote"
)
func main() {
fmt.Println(quote.Hello())
}
EOF
# 編譯執行
$ go build
$ ./hello
Hello, world.
1. 命令介紹
go mod init github.com/my/mod
用來初始化一個 module 並且生成一個 go.mod 文件。
$ go mod init github.com/my/hello
go: creating new go.mod: module github.com/my/hello
$ cat go.mod
module github.com/my/hello
go 1.12
go get github.com/some/pkg
下載最新版本的 module 以及它的所有依賴,並且在 go.mod 中增加對應的 require。go get
不需要被顯示執行,在執行go build
和go test
的時候,它會根據依賴自動執行。
$ go get github.com/sirupsen/logrus
go: finding github.com/sirupsen/logrus v1.3.0
go: finding github.com/davecgh/go-spew v1.1.1
go: finding golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33
go: finding github.com/stretchr/objx v0.1.1
go: finding golang.org/x/crypto v0.0.0-20180904163835-0709b304e793
go: finding github.com/konsorten/go-windows-terminal-sequences v1.0.1
go: finding github.com/pmezard/go-difflib v1.0.0
go: finding github.com/stretchr/testify v1.2.2
go: downloading github.com/sirupsen/logrus v1.3.0
go: extracting github.com/sirupsen/logrus v1.3.0
go: downloading golang.org/x/crypto v0.0.0-20180904163835-0709b304e793
go: extracting golang.org/x/crypto v0.0.0-20180904163835-0709b304e793
go: downloading golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33
go: extracting golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33
執行完之后, modules 的文件被下載到 $GOPATH/pkg/mod 下,並且按照 pkg@v1.0.1 的方式命名。
$ ls ~/go/pkg/mod/github.com/sirupsen
logrus@v1.3.0
ls ~/go/pkg/mod/golang.org/x/
crypto@v0.0.0-20180904163835-0709b304e793 sys@v0.0.0-20180905080454-ebe1bf3edb33 text@v0.0.0-20170915032832-14c0d48ead0c
go.mod 中增加了對應的 require:
$ cat go.mod
module github.com/my/hello
go 1.12
require github.com/sirupsen/logrus v1.2.0 // indirect
go get github.com/some/pkg@v1.0.1
下載指定版本的 module 以及它的所有依賴。
$ go get github.com/sirupsen/logrus@v1.2.0
go: finding github.com/sirupsen/logrus v1.2.0
go: downloading github.com/sirupsen/logrus v1.2.0
go: extracting github.com/sirupsen/logrus v1.2.0
此時在 $GOPATH/pkg/mod 中下載了對應的文件,並且 go.mod 的 require 發生了變化:
$ cat go.mod
module github.com/my/hello
go 1.12
require github.com/sirupsen/logrus v1.2.0 // indirect
go get -u github.com/some/pkg
更新次版本號,由於主版本號的不兼容,所以不會更新主版本號。go get -u=patch
更新修訂號go list -m all
查看所有依賴的 module 以及版本go list -u -m all
查看可用的次版本號和修訂號的更新go mod tidy
刪除 go.mod 中沒用到的 module
3. goproxy 的使用
國內用戶在用 golang 的時候經常會遇到一個問題,就是下不下來代碼。 在以前, 我們下載不了 googlesource.com 上的 go packages,通常都可以到 github 上面去克隆,然后放到 golang.org目錄下面就可以了。
但是 go module 的出現使我們的操作要變得很復雜了 (可以想象一下, 先 git clone
, 然后 git checkout v1.1.1
, 最后 copy 到 mod/pkg@v1.1.1 下)。
最簡單的方式是 export GOPROXY=https://goproxy.io
。 設置 go 代理,一切搞定!這樣下載的時候都通過 goproxy 來下載。
怎么發布不兼容版本?
根據前文的介紹,如果新版本不能兼容舊版本,那么就要使用新的主版本號和新的導入路徑 。
要提供新的主版本號並不困難,打個 tag 就是。
那么怎么來提供新的導入路徑呢?有兩種方式:
1. 就地修改
只需要將 go.mod 中的 module github.com/you/mod 修改成 github.com/you/mod/v2 。然后修改本 module 內的所有 import 語句,添加 /v2。如 import "github.com/you/mod/v2/mypkg"。
注意: 在 module 的 git(或者其他的版本控制) 倉庫中,存在所有的提交, 所以其他依賴 v1..版本的 module 會自動使用舊版本。而依賴 v2.. 版本的 module 將會從 github.com/you/mod/ 中下載對應的版本,並且將 github.com/you/mod/ 下的所有包的路徑對應成 github.com/you/mod/v2。
2. 創建子目錄
另外一種方式是在 module 下創建一個 v2 目錄, 然后將所有文件移動 v2 中,並且修改 go.mod 。 同時也需要修改所有相關的 import 語句。