Go 部落格

Go Modules:v2 及更高版本

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

引言

本文是系列文章的第 4 部分。

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

隨著成功專案的成熟和新需求的加入,過去的特性和設計決策可能會變得不再合理。開發者可能希望透過移除已廢棄的函式、重新命名型別或將複雜的包拆分成易於管理的部分來整合他們學到的經驗教訓。這些型別的更改需要下游使用者付出努力將其程式碼遷移到新的 API,因此在沒有仔細權衡利弊之前不應進行此類更改。

對於仍處於實驗階段的專案(主版本號為 v0),使用者會預料到偶爾會出現破壞性更改。對於已宣告穩定的專案(主版本號為 v1 或更高),破壞性更改必須在新主版本中進行。本文探討了主版本語義、如何建立和釋出新主版本,以及如何維護模組的多個主版本。

主版本和模組路徑

Modules 正式確定了 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 Modules 與大多數其他依賴管理系統不同的方式之一。需要字尾來解決鑽石依賴問題。在 Go Modules 之前,gopkg.in 允許包維護者遵循我們現在所說的匯入相容性規則。使用 gopkg.in,如果您依賴於匯入 gopkg.in/yaml.v1 的包以及另一個匯入 gopkg.in/yaml.v2 的包,則不會發生衝突,因為這兩個 yaml 包具有不同的匯入路徑——它們使用了版本字尾,與 Go Modules 類似。由於 gopkg.in 與 Go Modules 共享相同的版本字尾方法,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

這種方法與不瞭解 Modules 的工具相容:倉庫內的檔案路徑與 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'
$

現在,讓我們透過複製當前的 go.mod 檔案並在模組路徑中新增 /v2 字尾來建立一個 v2 版本的 go.mod 檔案

$ 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 十週年
上一篇文章:在 Go 1.13 中處理錯誤
部落格索引