Go 部落格

Go 模組:v2 及更高版本

Jean Barkhuysen 和 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'
$

現在,我們透過複製當前的 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 等)。

結論

主要版本更改會導致開發和維護開銷,並需要下游使用者投入以進行遷移。專案越大,這些開銷往往越大。只有在確定了令人信服的理由之後,才應進行主要版本更改。一旦確定了進行破壞性更改的令人信服的理由,我們建議在主分支中開發多個主要版本,因為它與更廣泛的現有工具相容。

v1+ 模組的破壞性更改應始終發生在新的 vN+1 模組中。當釋出新模組時,這意味著維護者以及需要遷移到新包的使用者會增加額外的工作。因此,維護者應在釋出穩定版本之前驗證其 API,並仔細考慮 v1 之後是否確實有必要進行破壞性更改。

下一篇文章:Go 十週年
上一篇文章:Go 1.13 中的錯誤處理
部落格索引