:::info 日期:2019 年 10 月 07 日
作者:Jean de Klerk and Tyler Bui-Palsulich
原文链接:https://go.dev/blog/v2-go-modules :::

介绍

这篇文章是系列文章的第 4 部分。

注意:有关开发模块的文档,请参阅开发和发布模块

随着成功项目的成熟和新需求的增加,过去的功能和设计决策可能不再有意义。 开发人员可能希望通过删除不推荐使用的函数、重命名类型或将复杂的包拆分为可管理的部分来整合他们学到的经验教训。 这些类型的更改需要下游用户努力将其代码迁移到新 API,因此不应在未仔细考虑收益大于成本的情况下进行更改。

对于仍处于试验阶段的项目——在主要版本 v0——用户预计偶尔会发生重大变化。 对于声明为稳定的项目——主要版本 v1 或更高版本——必须在新的主要版本中进行重大更改。 这篇文章探讨了主版本语义、如何创建和发布新的主版本,以及如何维护一个模块的多个主版本。

主要版本和模块路径

模块形式化了 Go 中的一个重要原则,即导入兼容性规则

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

根据定义,包的新主要版本不向后兼容以前的版本。 这意味着模块的新主要版本必须具有与以前版本不同的模块路径。 从 v2 开始,主要版本必须出现在模块路径的末尾(在 go.mod 文件的模块语句中声明)。 例如,模块 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+ 模块。

  1. github.com/googleapis/gax-go @ master branch
  2. /go.mod module github.com/googleapis/gax-go
  3. /v2/go.mod module github.com/googleapis/gax-go/v2

这种方法与不知道模块的工具兼容:存储库中的文件路径与 go get 在 GOPATH 模式下预期的路径相匹配。 此策略还允许在不同目录中一起开发所有主要版本。

其他策略可能会将主要版本保留在不同的分支上。 但是,如果 v2+ 源代码位于存储库的默认分支(通常是 master)上,则不知道版本的工具(包括 GOPATH 模式下的 go 命令)可能无法区分主要版本。

这篇文章中的示例将遵循主版本子目录策略,因为它提供了最大的兼容性。 我们建议模块作者遵循这个策略,只要他们有用户在 GOPATH 模式下开发。

发布 v2 和 beyond

这篇文章以 github.com/googleapis/gax-go 为例:

  1. $ pwd
  2. /tmp/gax-go
  3. $ ls
  4. CODE_OF_CONDUCT.md call_option.go internal
  5. CONTRIBUTING.md gax.go invoke.go
  6. LICENSE go.mod tools.go
  7. README.md go.sum RELEASING.md
  8. header.go
  9. $ cat go.mod
  10. module github.com/googleapis/gax-go
  11. go 1.9
  12. require (
  13. github.com/golang/protobuf v1.3.1
  14. golang.org/x/exp v0.0.0-20190221220918-438050ddec5e
  15. golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3
  16. golang.org/x/tools v0.0.0-20190114222345-bf090417da8b
  17. google.golang.org/grpc v1.19.0
  18. honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099
  19. )
  20. $

要在 github.com/googleapis/gax-go 的 v2 上开始开发,我们将创建一个新的 v2/ 目录并将我们的包复制到其中。

  1. $ mkdir v2
  2. $ cp *.go v2/
  3. building file list ... done
  4. call_option.go
  5. gax.go
  6. header.go
  7. invoke.go
  8. tools.go
  9. sent 10588 bytes received 130 bytes 21436.00 bytes/sec
  10. total size is 10208 speedup is 0.95
  11. $
  1. 现在,让我们通过复制当前 go.mod 文件并在模块路径中添加 v2/ 后缀来创建一个 v2 go.mod 文件:
  1. $ cp go.mod v2/go.mod
  2. $ go mod edit -module github.com/googleapis/gax-go/v2 v2/go.mod
  3. $
  1. 请注意,v2 版本被视为与 v0 / v1 版本不同的模块:两者可以在同一个构建中共存。 因此,如果您的 v2+ 模块有多个包,您应该更新它们以使用新的 /v2 导入路径:否则,您的 v2+ 模块将依赖于您的 v0 / v1 模块。 例如,要将所有 github.com/my/project 引用更新到 github.com/my/project/v2,您可以使用 find sed
  1. $ find . -type f \
  2. -name '*.go' \
  3. -exec sed -i -e 's,github.com/my/project,github.com/my/project/v2,g' {} \;
  4. $

现在我们有一个 v2 模块,但我们想在发布版本之前进行试验并进行更改。 在我们发布 v2.0.0(或任何没有预发布后缀的版本)之前,我们可以在决定新 API 时进行开发和重大更改。 如果我们希望用户能够在新 API 正式稳定之前对其进行试验,我们可以发布 v2 预发布版本:

  1. $ git tag v2.0.0-alpha.1
  2. $ git push origin v2.0.0-alpha.1
  3. $
  1. 一旦我们对 v2 API 感到满意并确定不需要任何其他重大更改,我们就可以标记 v2.0.0
  1. $ git tag v2.0.0
  2. $ git push origin v2.0.0
  3. $

那时,现在有两个主要版本需要维护。 向后兼容的更改和错误修复将导致新的次要版本和补丁版本(例如,v1.1.0、v2.0.1 等)。

总结

重大版本变更会导致开发和维护开销,需要下游用户投资才能迁移。 项目越大,这些开销往往越大。 只有在确定令人信服的原因后,才应进行主要版本更改。 一旦确定了重大更改的令人信服的原因,我们建议在主分支中开发多个主要版本,因为它与更广泛的现有工具兼容。

对 v1+ 模块的重大更改应始终发生在新的 vN+1 模块中。 当一个新模块发布时,对于维护者和需要迁移到新包的用户来说意味着额外的工作。 因此,维护者应该在发布稳定版本之前验证他们的 API,并仔细考虑在 v1 之后是否真的有必要进行重大更改。