Go 部落格

Go 模組:v2 及更新版本

Jean de Klerk 和 Tyler Bui-Palsulich
2019 年 11 月 7 日

簡介

這篇文章是系列文章的第 4 篇。

注意:有關開發模組的文件,請參閱 開發和發布模組

隨著成功專案逐漸成熟,並加入新的需求,過往的功能和設計決策可能會失去意義。開發人員可能會希望整合從中學到的經驗,包括移除已棄用的函數、重新命名類型或將複雜套件分割成可管理的區塊。此類變更需要下游使用者努力將其程式碼移轉至新的 API,因此務必審慎衡量好處是否大於成本後,才能進行變更。

對於仍處於實驗階段的專案——主版本為 v0——使用者預期偶爾會發生重大變更。對於宣告為穩定的專案——主版本為 v1 或更高——重大變更必須在新的主版本中執行。這篇文章探討主版本的語意、如何建立並發布新的主版本,以及如何維護模組的多個主版本。

主版本與模組路徑

模組在 Go 中將一項重要原則正式化,即 匯入相容性規則

If an old package and a new package have the same import path,
the new package must be backwards compatible with the old package.

根據定義,一個套件的新主版本不與先前版本向後相容。這意味著一個模組的新主版本必須有不同於先前版本的模組路徑。從 v2 開始,主版本必須出現在模組路徑的結尾(宣告於 go.mod 檔案中的 module 陳述)。例如,當 github.com/googleapis/gax-go 模組的作者開發 v2 時,他們使用了新的模組路徑 github.com/googleapis/gax-go/v2。想要使用 v2 的使用者必須將他們的套件匯入和模組需求改成 github.com/googleapis/gax-go/v2

需要主版本字尾是 Go 模組有別於大多數其他相依性管理系統的方法之一。需要字尾來解決 菱形相依性問題。在 Go 模組之前,gopkg.in 允許套件維護人員遵循我們現在所稱的匯入相容性規則。使用 gopkg.in,如果您依賴一個匯入 gopkg.in/yaml.v1 的套件和另一個匯入 gopkg.in/yaml.v2 的套件,不會發生衝突,因為兩個 yaml 套件有不同的匯入路徑——它們使用版本字尾,如同 Go 模組。因為 gopkg.in 與 Go 模組共用相同的版本字尾方法,所以 Go 指令將 gopkg.in/yaml.v2 中的 .v2 視為有效的版本字尾。這是為了相容 gopkg.in 的特別情況:在其他網域中託管的模組需要像 /v2 這樣的斜線字尾。

主版本策略

建議的策略是使用命名為依據主版本字尾的目錄來開發 v2+ 模組。

github.com/googleapis/gax-go @ master branch
/go.mod    → module github.com/googleapis/gax-go
/v2/go.mod → module github.com/googleapis/gax-go/v2

此方法與未認識模組的工具相容:儲存庫中的檔案路徑與 GOPATH 模式中的 go get 所預期的路徑相符。此策略還允許所有主版本同時在不同的目錄中開發。

其他策略可能會將主版本保留在不同的分支。然而,如果 v2+ 原始程式碼位於儲存庫的預設分支(通常為 master),對版本不敏感的工具——包括 GOPATH 模式中的 go 指令——可能無法區分主版本。

本文中的範例將遵循主版本子目錄策略,因為它提供最大的相容性。我們建議模組作者遵循此策略,只要他們的使用者會在 GOPATH 模式中開發。

刊登 v2 及之後的版本

這篇文章使用 github.com/googleapis/gax-go 作為範例

$ pwd
/tmp/gax-go
$ ls
CODE_OF_CONDUCT.md  call_option.go  internal
CONTRIBUTING.md     gax.go          invoke.go
LICENSE             go.mod          tools.go
README.md           go.sum          RELEASING.md
header.go
$ cat go.mod
module github.com/googleapis/gax-go

go 1.9

require (
    github.com/golang/protobuf v1.3.1
    golang.org/x/exp v0.0.0-20190221220918-438050ddec5e
    golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3
    golang.org/x/tools v0.0.0-20190114222345-bf090417da8b
    google.golang.org/grpc v1.19.0
    honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099
)
$

要開始開發 github.com/googleapis/gax-gov2 版本,我們會建立一個新的 v2/ 目錄,並將套件複製到該目錄中。

$ mkdir v2
$ cp -v *.go v2
'call_option.go' -> 'v2/call_option.go'
'gax.go' -> 'v2/gax.go'
'header.go' -> 'v2/header.go'
'invoke.go' -> 'v2/invoke.go'
$

現在,讓我們建立一個 v2 go.mod 檔案,方法是複製目前的 go.mod 檔案,然後在模組路徑中加入 /v2 字尾

$ cp go.mod v2/go.mod
$ go mod edit -module github.com/googleapis/gax-go/v2 v2/go.mod
$

請注意,v2 版本視為與 v0 / v1 版本不同的獨立模組:這兩個版本可以在同一個建置中同時並存。因此,如果你的 v2+ 模組有多個套件,你應該將它們更新為使用新的 /v2 匯入路徑:否則,你的 v2+ 模組會依賴於你的 v0 / v1 模組。例如,若要將所有 github.com/my/project 參考更新為 github.com/my/project/v2,你可以使用 findsed

$ find . -type f \
    -name '*.go' \
    -exec sed -i -e 's,github.com/my/project,github.com/my/project/v2,g' {} \;
$

現在我們有了 v2 模組,但是我們希望在釋出版本之前先實驗和變更。在我們釋出 v2.0.0 時(或是任何沒有前置發行字尾的版本),我們可以繼續開發並暫停使用變更,因為我們會決定新的 API。如果我們希望使用者能在新 API 穩定之前先對其進行體驗,我們可以釋出一個 v2 前置發行版本

$ git tag v2.0.0-alpha.1
$ git push origin v2.0.0-alpha.1
$

當我們對 v2 API 感到滿意,並確定不用暫停任何其他變更時,我們就可以標記 v2.0.0

$ git tag v2.0.0
$ git push origin v2.0.0
$

在那個時點,現在需要維護兩個主要版本。向下相容的變更和錯誤修正程式會產生新的次要和修補版本(例如,v1.1.0v2.0.1 等)。

結論

主要版本變更會產生開發和維護的負擔,而且需要下游使用者投資以進行移轉。在專案越大時,這些負擔會越大。只有在找出令人信服的理由後,才應變更主要版本。一旦找出一個令人信服的暫停使用變更理由,我們建議在 master 分支中開發多個主要版本,這是因為它與更多不同種類的現有工具相容。

v1+ 模組的暫停使用變更應始終在一個新的 vN+1 模組中發生。當一個新的模組釋出時,這表示維護人員和需要移轉到新套件的使用者會有額外的負擔。因此,維護人員應在發布穩定版本之前驗證自己的 API,並審慎考慮暫停使用變更是否在 v1 之後真的必要。

下一篇文章:Go 滿 10 歲了
上一篇文章:使用 Go 1.13 中的錯誤
部落格索引