Golang依赖包管理知多少(4)-v2与后续版本

前言

成功的项目随着逐步成熟以及新功能的加入,过去的特性和设计方案可能不能满足需求。开发者可能想整合一下他们所学的知识比如移除废弃函数、重命名参数或者分割复杂的包为多个可维护模块。这些变更都需要下游用户付出努力来迁移他们的代码来适配新的API,所以他们不会没有认真权衡过收益和成本而做出变更的。

对于一些还在实验中的项目,比如主版本为v0的项目,用户会预期到偶尔的重大变更。对于已经发布为稳定版本的项目,比如主版本为v1或者更高版本的项目,重大的变更需要在一个大版本上进行。这篇文章主要介绍了大版本策略、如果创建并发布一个新的大版本以及如何维护一个module的多个主版本。

主版本与module path

modules模式规范了一个重要的原则,导入兼容性原则:

如果一个老版本的包和一个新版本的包使用相同的导入path,那么新版本的包必须向后兼容老版本的包。

一个新的大版本的包不会向后兼容之前的版本。这就意味着一个新的大版本module必须有一个不同于历史版本的module path。从v2版本开始,大版本号必须紧跟在module path后面(声明在go.mod文件的module语句后面)。比如,当一名开发者开发了github.com/googleapis/gax-go module的v2时,他需要使用新的module path-github.com/googleapis/gax-go/v2,如果用户想使用这个module的v2版本就必须要改变他们的包导入以及module依赖到github.com/googleapis/gax-go/v2

大版本后缀在module path上是module模式不同于其他依赖管理工具的地方。后缀方式可以解决菱形依赖问题。在module模式之前,gopkg.in允许软件维护者遵循我们现在的这套导入兼容性规则,如果你的module依赖一个包gopkg.in/yaml.v1,另外的一个module依赖gopkg.in/yaml.v2,这时是不会有冲突的,因为这两个yaml包使用了不同的module path,和module模式类似 gopkg.in也适用了后缀方式。由于gopkg.in使用了module模式相同的后缀版本技术,所以go命令兼容gopkg.in.v2作为版本后缀,但这仅针对gopkg.in,托管在其他域名下的module还是需要使用/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

这样做并不是为了module模式,主要为了兼容一些工具:在仓库中的文件路径正好是GOPATH模式中go get中所需要的路径。这种方式也方便把所有的大版本按照不同的目录来进行开发。

其他的策略也有按照不同的大版本放在不同的分支,但是如果v2+版本的源代码放在仓库的默认分支(通常是master),像GOPATH下的go这类的工具又不识别版本可能就不能区分不同的大版本。

由于不同的大版本放在不同的子目录这一策略的兼容性好,本文的例子将会遵循这个策略。如果他们让用户维持GOPATH模式,那我们建议module的开发者遵循这一策略。

发布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 *.go v2/
building file list ... done
call_option.go
gax.go
header.go
invoke.go
tools.go

sent 10588 bytes  received 130 bytes  21436.00 bytes/sec
total size is 10208  speedup is 0.95
$

现在我们通过拷贝原有的go.mod文件创建一个v2版本的go.mod文件,并且修改module path的定义,添加v2后缀:

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

注意这个v2版本是与v0、v1版本独立的module,在同一个构建中是可以共存的,所以如果你v2+版本的module有多个包,你需要把它们的导入路径都更新到v2版本,否则你v2+版本的module将会依赖v0、v1版本的module了。比如,为了更新所有github.com/my/projectgithub.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版本的module了,但是我们想在发布之前做一些预发布与更改。在我们发布v2.0.0之前可以做出我们想开发的新API的重大变更,但如果我们想让用户能够体验到预发布版本而不是正式稳定版本,我们可以发布一个预览版本:

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

一旦我们对v2版本的API满意而且也不需要做出重大变更了,我们可以标记v2.0.0的tag了:

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

到这里,就有两个大版本在线上了。向后兼容的变更或者bug修复会通过小版本或者patch版本来发布,比如v1.1.0v2.0.1等等。

综述

大版本的变更会带来开发和维护的开销,同时也需要下游用户为了迁移而投入。项目越大,开销越大。大版本的变更应当有一个令人信服的原因。对于一个重大的变更一旦被说服,我们建议你在主分支同时开发多个大版本因为这对于已经存在的工具集来说是可兼容的。

v1版本以后的module的重大变更会出现一个新的vN+1的module。当一个新的module版本发布时,这对维护者和哪些想升级到新版本的用户来说都是额外的工作量。因此module维护者在发布一个稳定版本时一定要验证这些API,在以后的版本中小心谨慎的评估是否需要重大的变更。