[Go] 到底 go get 的版號怎麼運作的?
在 Go Module 模式底下使用 go get
取得套件的時候常常會有各種版號出現,例如 v1.0.2、v2.0.3 +incompatible、甚至是一段 hash v0.0.0-20200226145339-3e397ee01bc6。 還有取得 v2 版本的時候,有時候是 go get github.com/my/foo@v2.2.0
有時候是 go get github.com/my/foo/v2@v2.2.0
。 為了搞懂這些差別,我參考了一些官方文章還有做了一些實驗,今天就來解說這些差別到底是什麼情況。
在閱讀此文章之前,建議要先了解
- 什麼是 Go Module。 Ref: Using Go Modules
- 如何更改主版號(如: v1.0.0 升級成 v2.0.0)。 Ref: Go Modules: v2 and Beyond
- v1.2.3 版號的意義。 Ref: Semantic Versioning 2.0.0
Semantic import version
在文章開始前需簡短說明一下什麼是 Semantic Import Versioning。
根據 Semantic Versioning 版本建議規則,當主版號變更的時候,代表有破壞性更新。 以 Go 來說,如果這時候更新的版本還是用同樣的路徑,會造成使用的人一更新套件(package)就會壞掉。 所以會建議使用新的路徑讓兩個版本並行,並且在採用漸進式升級的方式升級成新的版本。 例如 v1.x.x 的路徑會是 import github.com/my/foo
,主版號升級到 v2 後,路徑會是 import github.com/my/foo/v2
。 而這種不同版本相容的 import 方式稱為 Semantic Import Versioning。
當 Go Module 啟用後,會強制按照 Semantic Import Versioning 的規則,履行下列義務
- 版本號需要使用 Semantic Versioning 的方式。 例如 v1.2.3, v2.7.1。
- 當版本號大於等於 v2+ 的時候,在
go.mod
的 module path 要加上 /vN,例如module github.com/my/foo/v2
。 此時的要使用foo/mypkg
套件的時候, import 會變成import github.com/my/foo/v2/mypkg
。 - 如果主版號是 v0 或 v1 則 module path 與 import path 不需要加上 /v0 或 /v1
有了上敘的知識後,就開始我們今天的故事吧!!
都跟取得套件的 Repository 有關係
這些 go get
版本的路徑要怎麼指定,差別都是來自於你要 go get
那個目標的 Repository。 我目前歸納出來有四個因素會影響 go get
的版號
- Commit
- 標籤(tag)。
go get
會參考 tag 當作版號,例如 v.1.2.3 - 是否啟用 Go Module
- 什麼時候啟用 Go Module
接下來我會分三個情境來解說這些差別
- 如果目標 Repository 沒有啟用的 Go Module
- 如果目標 Repository 沒有啟用的 Go Module,但是有 v2+ 以上版本會發生什麼事情?
- 如果在 v2+ 版本以上啟用 Go Module 的時候會發生什麼事情?
如果目標 Repository 沒有啟用的 Go Module
如果你的目標 Repository 沒有啟用的 Go Module 的話,他會根據 該 Repository 的 tag 版本,下載最後一版 (Version Selection)。 例如該 Repository 有 v3.1.2, v2.1.1, v1.2.5, v1.1.0 這 4 個 tag ,執行 go get github.com/my/foo@latest
的話,他會下載 v3.1.2 那個 tag 的版本。
假設該 Repository 連 tag 都沒有的話,則採用 Pseudo-versions 的方式,會去抓取 master 分支的最後一個 commit,然後你就會看到該版本號的資訊會類似 v0.0.0-20200824153131-fdc22cc4ae4b 這種格式,他是由 v0.0.0-{commit 時間}-{commit hash} 所組成的。
如果目標 Repository 沒有啟用的 Go Module,但是有 v2 以上版本會發生什麼事情?
在看一些 Go Module 文章的時候,例如 Using Go Modules,裡面有提到,啟用 Go Module 後,如果是 v2 以上版本的時候,需要加上 /vN
路徑,例如 go get github.com/my/foo/v2
,這是當作跟 v1 是不同路徑的方式來抓 v2 的版本。 那如果目標 Repository 沒有啟用 Go Module,都採用 tag 的方式標示版本,會發生什麼事情呢?
這種情況底下,的做法就不是使用 /v2
,而是使用 @
的方式指定版本就可以了,例如 go get github.com/my/foo@v2.0.0
,因為沒有啟用 Go Module 的模式,所以所有版本都是在同一個路徑 github.com/my/foo
底下 。
當下載 v2 以上版本的時候會看到 go.mod 紀錄的套件會有 +incompatible
這個字樣,例如 require github.com/my/foo v3.0.1+incompatible
,為什麼會有 +incompatible
呢?
為什麼有 +incompatible?
我們要從 Go 的 FAQ - How should I manage package versions using go get? 說起,這是 Go 的建議多年的套件管控機制,裡面有一段話說
Packages intended for public use should try to maintain backward compatibility as they evolve. The Go 1 compatibility guidelines are a good reference here: don’t remove exported names, encourage tagged composite literals, and so on. If different functionality is required, add a new name instead of changing an old one. If a complete break is required, create a new package with a new import path
上面畫線的重點是,如果新的版本有包含破壞性更新,則建立一個新的 package import path ,例如 github.com/my/foo/v2
。 這是因為當使用同一個 package import path 時要能夠確保相容性,不能讓使用者更新套件後,程式就壞掉。 而這建議也是從 Semantic Import Versioning 來的。
Go Module 也有根據這個原則設計,啟用 Go Module 後有 三個原則 需要遵守
- import path 不一樣會當成不同的套件
- 例如
github.com/my/foo
vsgithub.com/my/foo/v2
是不一樣的套件
- 例如
- import path 如果沒有包含
/v2+
的路徑的話,會當作 v1 或 v0 版本 - import path 是從 go.mod 裡面的 module path 定義的,如
module github.com/my/foo/v2
簡短來說, +incompatible 是指說「當使用的套件沒有啟用 Go Module 以及其版本號大於 v1,也就是 v2+ 的時候,我們 import 的路徑會是 import github.com/my/foo
,這時候根據上敘的原則2, Go Module 會將其看待成 v1 或 v0 版本,這就造成與原則2產生衝突。。」
這情況下, Go Module 會 認為該套件並沒有使用 Semantic Import Versioning 的方式來管控套件 ,就會認為他跟 v1 版本是不相容的,所以加上 +incompatible 當作警告,但不影響使用。
如果在 v2+ 版本以上啟用 Go Module 的時候會發生什麼事情?
現在假設有一個情況,有一個遠端的 Repository get github.com/my/foo
一直沒有啟用 Go Module,而且已經存在了好幾版的 tag,例如 v1.2.1、v1.3.5、v1.3.7、v2.1.9、v2.4.7、v3.0.1。 在這情況下 go get github.com/my/foo@latest
自然會取得 v3.0.1+incompatible 的版本。
這時候 Repository github.com/my/foo
的作者又有新版 v4.0.0 想要釋出,他可以有兩個選擇
繼續不啟用 Go Module ,新增 tag v4.0.0 版本。 在這情況下,當然
go get github.com/my/foo@latest
就會取得 v4.0.0+incompatible 的版本決定啟用 Go Module 改用符合 Semantic Import Versioning 的規則管理套件,然後在這情況下釋出
v4.0.0
版本。
在第二個選擇的情況下,go get github.com/my/foo@latest
你還是會取得 v3.0.1+incompatible,因為根據 Semantic Import Versioning 的原則,主要版本變更會要求 不同路徑,要用 go get github.com/my/foo/v4
才可以取得 v4.0.0 的版本,而之後所有的 v4 版本都會釋出在 github.com/my/foo/v4
這個路徑底下。
如果這時候新增一個 v3.2.3 的版本會發生麼事情?
在沒有建立 go get github.com/my/foo/v3
的路徑,直接新增 v3.2.0 tag 的情況下,是沒辦法 go get github.com/my/foo@v3.2.0
版本。會出現這種錯誤訊息
這是因為,當啟用 Go Module 後的那個 commit 就是一個切割點 。 在那個 commit 之前的版本 v1.2.1、v1.3.5、v1.3.7、v2.1.9、v2.4.7、v3.0.1 都還是放在 github.com/my/foo
這個路徑底下,所以你還是可以 go get github.com/my/foo@v3.0.1
。 但是在這 commit 之後,所有規則請按照 Semantic Import Versioning 來走。 想要新增一個 v3 的版本? 請放在 github.com/my/foo/v3
底下。
當作者有按照規則走的時候,你就可以 go get github.com/my/foo/v3
取得 v3.2.0 版本了。
在這規則底下,就是每一個主版本都放在他該存在的路徑,v2.x.x 放在 github.com/my/foo/v2
路徑, v3.x.x 放在 github.com/my/foo/v3
路徑,v1.x.x 就放在 github.com/my/foo
路徑。
小結
這些就是 go get
各種不同版號的運作機制啦。 這也是為什麼很多人,啟用 Go Module 後會直接跳一個主版本,否則可能會發生 v3 的舊版本的在 github.com/my/foo
路徑,啟用 Go Module 後的 v3 新版本會在 github.com/my/foo/v3
路徑。 到不如就把 v3 以前都放在 github.com/my/foo
未來所有版本都開始分路徑 github.com/my/foo/v4+
。
延伸閱讀
[Go Modules wiki]
[The Go Blog - Using Go Modules]
[FAQs — Semantic Import Versioning]