在 Go Module 模式底下使用 go get 取得套件的時候常常會有各種版號出現,例如 v1.0.2v2.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。 為了搞懂這些差別,我參考了一些官方文章還有做了一些實驗,今天就來解說這些差別到底是什麼情況。

在閱讀此文章之前,建議要先了解

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 的規則,履行下列義務

  1. 版本號需要使用 Semantic Versioning 的方式。 例如 v1.2.3, v2.7.1
  2. 當版本號大於等於 v2+ 的時候,在 go.modmodule path 要加上 /vN,例如 module github.com/my/foo/v2。 此時的要使用 foo/mypkg 套件的時候, import 會變成 import github.com/my/foo/v2/mypkg
  3. 如果主版號是 v0v1module pathimport path 不需要加上 /v0/v1

有了上敘的知識後,就開始我們今天的故事吧!!

都跟取得套件的 Repository 有關係

這些 go get 版本的路徑要怎麼指定,差別都是來自於你要 go get 那個目標的 Repository。 我目前歸納出來有四個因素會影響 go get 的版號

  1. Commit
  2. 標籤(tag)。go get 會參考 tag 當作版號,例如 v.1.2.3
  3. 是否啟用 Go Module
  4. 什麼時候啟用 Go Module

接下來我會分三個情境來解說這些差別

  1. 如果目標 Repository 沒有啟用的 Go Module
  2. 如果目標 Repository 沒有啟用的 Go Module,但是有 v2+ 以上版本會發生什麼事情?
  3. 如果在 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 後有 三個原則 需要遵守

  1. import path 不一樣會當成不同的套件
    • 例如 github.com/my/foo vs github.com/my/foo/v2 是不一樣的套件
  2. import path 如果沒有包含 /v2+ 的路徑的話,會當作 v1v0 版本
  3. 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 會將其看待成 v1v0 版本,這就造成與原則2產生衝突。。」

這情況下, Go Module 會 認為該套件並沒有使用 Semantic Import Versioning 的方式來管控套件 ,就會認為他跟 v1 版本是不相容的,所以加上 +incompatible 當作警告,但不影響使用。

如果在 v2+ 版本以上啟用 Go Module 的時候會發生什麼事情?

現在假設有一個情況,有一個遠端的 Repository get github.com/my/foo 一直沒有啟用 Go Module,而且已經存在了好幾版的 tag,例如 v1.2.1v1.3.5v1.3.7v2.1.9v2.4.7v3.0.1。 在這情況下 go get github.com/my/foo@latest 自然會取得 v3.0.1+incompatible 的版本。

這時候 Repository github.com/my/foo 的作者又有新版 v4.0.0 想要釋出,他可以有兩個選擇

  1. 繼續不啟用 Go Module ,新增 tag v4.0.0 版本。 在這情況下,當然 go get github.com/my/foo@latest 就會取得 v4.0.0+incompatible 的版本

  2. 決定啟用 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 版本。會出現這種錯誤訊息

invalid version: module contains a go.mod file, so major version must be compatible: should be v0 or v1, not v3invalid version: module contains a go.mod file, so major version must be compatible: should be v0 or v1, not v3

這是因為,當啟用 Go Module 後的那個 commit 就是一個切割點 。 在那個 commit 之前的版本 v1.2.1v1.3.5v1.3.7v2.1.9v2.4.7v3.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]