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 中处理错误
博客索引