[译] 版本化 Go 之旅

345 阅读4分钟
原文链接: lingchao.xin

本文译自 A Tour of Versioned Go (vgo), Go & Versioning 的第 2 部分, 版权@归原文所有.

对我而言, 设计意味着构建, 拆除和再次构建, 一遍又一遍. 为了编写新的版本控制提案, 我构建了一个原型 vgo, 来处理许多细微的细节. 这篇博文展示了如何使用 vgo.

你现在可以通过运行 go get golang.org/x/vgo 下载并尝试 vgo. Vgogo 命令的一个直接替换(和分支拷贝). 你运行 vgo 而不是 go, 它将使用你安装在 $GOROOT (Go 1.10 beta1 或更高版本) 的编译器和标准库.

随着我们更多地了解什么可行, 什么不可行, vgo 的语义和命令行细节可能会发生变化. 但是, 我们打算避免 go.mod 文件格式的向后不兼容的更改, 以便今天添加了 go.mod 的项目以后也可以工作. 在我们完善提案时, 我们也会相应地更新 vgo.

示例

该部分演示怎么使用 vgo. 请按照步骤进行实验.

从安装 vgo 开始:

$ go get -u golang.org/x/vgo

你一定会遇到有趣的 bug, 因为 vgo 现在最多只有轻微的测试. 请使用 Go 问题跟踪 进行 bug 上报, 标题以 "x/vgo" 开头. 多谢.

Hello, world

我们来写一个有趣的 "Hello, world" 程序. 在 GOPATH/src 目录之外创建一个目录并切换到它:

$ cd $HOME
$ mkdir hello
$ cd hello

然后创建一个 hello.go:

package main // import "github.com/you/hello"

import (
    "fmt"
    "rsc.io/quote"
)

func main() {
    fmt.Println(quote.Hello())
}

或者下载它:

$ curl -sS https://swtch.com/hello.go >hello.go

创建一个空的 go.mod 文件来标记此模块的根目录, 然后构建并运行新程序:

$ echo >go.mod
$ vgo build
vgo: resolving import "rsc.io/quote"
vgo: finding rsc.io/quote (latest)
vgo: adding rsc.io/quote v1.5.2
vgo: finding rsc.io/quote v1.5.2
vgo: finding rsc.io/sampler v1.3.0
vgo: finding golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
vgo: downloading rsc.io/quote v1.5.2
vgo: downloading rsc.io/sampler v1.3.0
vgo: downloading golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
$ ./hello
Hello, world.
$

注意这里没有显式的需要运行 vgo get. 普通的 vgo build 将在遇到未知导入时查找包含它的模块, 并将该模块的最新版本作为依赖添加到当前模块中.

运行任何 vgo 命令的一个副作用是必要时会更新 go.mod. 这种情况下, vgo build 会写入新的 go.mod 文件:

$ cat go.mod
module "github.com/you/hello"

require "rsc.io/quote" v1.5.2
$

由于 go.mod 已写入, 下一次 vgo build 将不会再次解析导入或打印那么多:

$ vgo build
$ ./hello
Hello, world.
$

即使明天发布了 rsc.io/quote v1.5.3v1.6.0, 该目录中的构建仍将继续使用 v1.5.2, 除非进行明确的升级(见下文).

go.mod 文件列举了依赖的最小集合, 忽略了已列举中所隐含的. 在这种情况下, rsc.io/quote v1.5.2 依赖特定版本的 rsc.io/samplergolang.org/x/text, 所以在 go.mod 中重复列举它们是冗余的.

使用 vgo list -m 仍然可以找到构建所需的全套模块:

$ vgo list -m
MODULE                VERSION
github.com/you/hello  -
golang.org/x/text     v0.0.0-20170915032832-14c0d48ead0c
rsc.io/quote          v1.5.2
rsc.io/sampler        v1.3.0
$

此时你可能想知道为什么我们简单的 "hello world" 程序会使用 golang.org/x/text. 实际上 rsc.io/quote 依赖 rsc.io/sampler, 后者又依赖 golang.org/x/text 进行 language matching .

$ LANG=fr ./hello
Bonjour le monde.
$

升级

我们已经看到, 当必须将新模块添加到构建以解决新的导入时, vgo 会采用最新的模块. 此前, 它需要 rsc.io/quote, 并发现 v1.5.2 是最新的. 但除了解析新的导入, vgo 仅使用 go.mod 文件中列出的版本. 在我们的例子中, rsc.io/quote 间接依赖于 golang.org/x/textrsc.io/sampler 的特定版本. 事实证明, 这两个软件包都有较新的版本, 正如我们通过 vgo list -u (检查更新的软件包)看到的那样:

$ vgo list -m -u
MODULE                VERSION                             LATEST
github.com/you/hello  -                                   -
golang.org/x/text     v0.0.0-20170915032832-14c0d48ead0c  v0.0.0-20180208041248-4e4a3210bb54
rsc.io/quote          v1.5.2 (2018-02-14 10:44)           -
rsc.io/sampler        v1.3.0 (2018-02-13 14:05)           v1.99.99 (2018-02-13 17:20)
$

这两个软件包都有更新的版本, 所以我们可能想在我们的 hello 程序中升级它们.

首先升级 golang.org/x/text:

$ vgo get golang.org/x/text
vgo: finding golang.org/x/text v0.0.0-20180208041248-4e4a3210bb54
vgo: downloading golang.org/x/text v0.0.0-20180208041248-4e4a3210bb54
$ cat go.mod
module "github.com/you/hello"

require (
    "golang.org/x/text" v0.0.0-20180208041248-4e4a3210bb54
    "rsc.io/quote" v1.5.2
)
$

vgo get 命令将查找给定模块的最新版本, 并通过更新 go.mod 来将该版本添加为当前模块的依赖. 从现在开始, 未来的构建将使用较新的 text 模块:

$ vgo list -m
MODULE                VERSION
github.com/you/hello  -
golang.org/x/text     v0.0.0-20180208041248-4e4a3210bb54
rsc.io/quote          v1.5.2
rsc.io/sampler        v1.3.0
$

当然, 升级之后, 测试一切仍然工作良好是个好主意. 我们的依赖 rsc.io/quotersc.io/sampler 尚未使用较新的 text 模块进行测试. 我们可以在我们创建的配置中运行他们的测试:

$ vgo test all
?       github.com/you/hello    [no test files]
?       golang.org/x/text/internal/gen  [no test files]
ok      golang.org/x/text/internal/tag  0.020s
?       golang.org/x/text/internal/testtext [no test files]
ok      golang.org/x/text/internal/ucd  0.020s
ok      golang.org/x/text/language  0.068s
ok      golang.org/x/text/unicode/cldr  0.063s
ok      rsc.io/quote    0.015s
ok      rsc.io/sampler  0.016s
$

在原版 go 命令中, 软件包模式 all 意味着 GOPATH 中能找到的所有软件包. 这几乎总是太多而无用. 在 vgo 中, 我们已经将 all 的含义缩小为 "当前模块中的所有软件包, 以及它们以递归方式导入的软件包". rsc.io/quote 模块的 1.5.2 版本包含一个 buggy 包:

$ vgo test rsc.io/quote/...
ok      rsc.io/quote    (cached)
--- FAIL: Test (0.00s)
    buggy_test.go:10: buggy!
FAIL
FAIL    rsc.io/quote/buggy  0.014s
(exit status 1)
$

然而, 除非我们模块中的某个包导入 buggy, 否则它是不相干的, 所以它不包含在 all 里面. 无论如何, 升级的 x/text 看起来可以工作. 此时我们多半可以提交 go.mod.

另一种选择是使用 vgo get -u 升级构建所需的所有模块:

$ vgo get -u
vgo: finding golang.org/x/text latest
vgo: finding rsc.io/quote latest
vgo: finding rsc.io/sampler latest
vgo: finding rsc.io/sampler v1.99.99
vgo: finding golang.org/x/text latest
vgo: downloading rsc.io/sampler v1.99.99
$ cat go.mod
module "github.com/you/hello"

require (
    "golang.org/x/text" v0.0.0-20180208041248-4e4a3210bb54
    "rsc.io/quote" v1.5.2
    "rsc.io/sampler" v1.99.99
)
$

在这里, vgo get -u 保留了升级后的 text 模块, 并将 rsc.io/sampler 升级到其最新版本 v1.99.99.

让我们来运行测试:

$ vgo test all
?       github.com/you/hello    [no test files]
?       golang.org/x/text/internal/gen  [no test files]
ok      golang.org/x/text/internal/tag  (cached)
?       golang.org/x/text/internal/testtext [no test files]
ok      golang.org/x/text/internal/ucd  (cached)
ok      golang.org/x/text/language  0.070s
ok      golang.org/x/text/unicode/cldr  (cached)
--- FAIL: TestHello (0.00s)
    quote_test.go:19: Hello() = "99 bottles of beer on the wall, 99 bottles of beer, ...", want "Hello, world."
FAIL
FAIL    rsc.io/quote    0.014s
--- FAIL: TestHello (0.00s)
    hello_test.go:31: Hello([en-US fr]) = "99 bottles of beer on the wall, 99 bottles of beer, ...", want "Hello, world."
    hello_test.go:31: Hello([fr en-US]) = "99 bottles of beer on the wall, 99 bottles of beer, ...", want "Bonjour le monde."
FAIL
FAIL    rsc.io/sampler  0.014s
(exit status 1)
$

看起来 rsc.io/sampler v1.99.99 出了问题. 果然:

$ vgo build
$ ./hello
99 bottles of beer on the wall, 99 bottles of beer, ...
$

vgo get -u 获取每个依赖的最新版本的行为正和 go get 下载所有不在 GOPATH 的包所做的一样. 在一个 GOPATH 里空无一物的系统上:

$ go get -d rsc.io/hello
$ go build -o badhello rsc.io/hello
$ ./badhello
99 bottles of beer on the wall, 99 bottles of beer, ...
$

重要的区别是, 默认情况下, vgo 不会以这种方式运行. 你也可以通过降级撤消它.

降级

要降级软件包, 请使用 vgo list -t 显示可用的标记(tag)版本:

$ vgo list -t rsc.io/sampler
rsc.io/sampler
    v1.0.0
    v1.2.0
    v1.2.1
    v1.3.0
    v1.3.1
    v1.99.99
$

然后使用 vgo 获取要求的特定版本, 例如 v1.3.1:

$ cat go.mod
module "github.com/you/hello"

require (
    "golang.org/x/text" v0.0.0-20180208041248-4e4a3210bb54
    "rsc.io/quote" v1.5.2
    "rsc.io/sampler" v1.99.99
)
$ vgo get rsc.io/sampler@v1.3.1
vgo: finding rsc.io/sampler v1.3.1
vgo: downloading rsc.io/sampler v1.3.1
$ vgo list -m
MODULE                VERSION
github.com/you/hello  -
golang.org/x/text     v0.0.0-20180208041248-4e4a3210bb54
rsc.io/quote          v1.5.2
rsc.io/sampler        v1.3.1
$ cat go.mod
module "github.com/you/hello"

require (
    "golang.org/x/text" v0.0.0-20180208041248-4e4a3210bb54
    "rsc.io/quote" v1.5.2
    "rsc.io/sampler" v1.3.1
)
$ vgo test all
?       github.com/you/hello    [no test files]
?       golang.org/x/text/internal/gen  [no test files]
ok      golang.org/x/text/internal/tag  (cached)
?       golang.org/x/text/internal/testtext [no test files]
ok      golang.org/x/text/internal/ucd  (cached)
ok      golang.org/x/text/language  (cached)
ok      golang.org/x/text/unicode/cldr  (cached)
ok      rsc.io/quote    0.016s
ok      rsc.io/sampler  0.015s
$

降级一个软件包可能需要降级其他软件包. 例如:

$ vgo get rsc.io/sampler@v1.2.0
vgo: finding rsc.io/sampler v1.2.0
vgo: finding rsc.io/quote v1.5.1
vgo: finding rsc.io/quote v1.5.0
vgo: finding rsc.io/quote v1.4.0
vgo: finding rsc.io/sampler v1.0.0
vgo: downloading rsc.io/sampler v1.2.0
$ vgo list -m
MODULE                VERSION
github.com/you/hello  -
golang.org/x/text     v0.0.0-20180208041248-4e4a3210bb54
rsc.io/quote          v1.4.0
rsc.io/sampler        v1.2.0
$ cat go.mod
module "github.com/you/hello"

require (
    "golang.org/x/text" v0.0.0-20180208041248-4e4a3210bb54
    "rsc.io/quote" v1.4.0
    "rsc.io/sampler" v1.2.0
)
$

在这种情况下, rsc.io/quote v1.5.0 是第一个需要 rsc.io/sampler v1.3.0 的版本; 早期版本只需要 v1.0.0(或更高版本). 降级选择了 rsc.io/quote v1.4.0, 这是与 v1.2.0 兼容的最新版本.

也可以通过指定 none 作为版本来完全删除一个依赖, 这是一种极端的降级形式:

$ vgo get rsc.io/sampler@none
vgo: downloading rsc.io/quote v1.4.0
vgo: finding rsc.io/quote v1.3.0
$ vgo list -m
MODULE                VERSION
github.com/you/hello  -
golang.org/x/text     v0.0.0-20180208041248-4e4a3210bb54
rsc.io/quote          v1.3.0
$ cat go.mod
module "github.com/you/hello"

require (
    "golang.org/x/text" v0.0.0-20180208041248-4e4a3210bb54
    "rsc.io/quote" v1.3.0
)
$ vgo test all
vgo: downloading rsc.io/quote v1.3.0
?       github.com/you/hello    [no test files]
ok      rsc.io/quote    0.014s
$

让我们回到一切都是最新版本的状态, 包括 rsc.io/sampler v1.99.99:

$ vgo get -u
vgo: finding golang.org/x/text latest
vgo: finding rsc.io/quote latest
vgo: finding rsc.io/sampler latest
vgo: finding golang.org/x/text latest
$ vgo list -m
MODULE                VERSION
github.com/you/hello  -
golang.org/x/text     v0.0.0-20180208041248-4e4a3210bb54
rsc.io/quote          v1.5.2
rsc.io/sampler        v1.99.99
$

排除 (Excluding)

在确定 v1.99.99 并不适用于我们的 hello world 程序后, 我们可能想记录下这个事实, 以避免将来出现问题. 我们可以通过向 go.mod 添加 exclude 指令来做到这一点:

exclude "rsc.io/sampler" v1.99.99

之后的操作表现的好像该模块不存在一样:

$ echo 'exclude "rsc.io/sampler" v1.99.99' >>go.mod
$ vgo list -t rsc.io/sampler
rsc.io/sampler
    v1.0.0
    v1.2.0
    v1.2.1
    v1.3.0
    v1.3.1
    v1.99.99 # excluded
$ vgo get -u
vgo: finding golang.org/x/text latest
vgo: finding rsc.io/quote latest
vgo: finding rsc.io/sampler latest
vgo: finding rsc.io/sampler latest
vgo: finding golang.org/x/text latest
$ vgo list -m
MODULE                VERSION
github.com/you/hello  -
golang.org/x/text     v0.0.0-20180208041248-4e4a3210bb54
rsc.io/quote          v1.5.2
rsc.io/sampler        v1.3.1
$ cat go.mod
module "github.com/you/hello"

require (
    "golang.org/x/text" v0.0.0-20180208041248-4e4a3210bb54
    "rsc.io/quote" v1.5.2
    "rsc.io/sampler" v1.3.1
)

exclude "rsc.io/sampler" v1.99.99
$ vgo test all
?       github.com/you/hello    [no test files]
?       golang.org/x/text/internal/gen  [no test files]
ok      golang.org/x/text/internal/tag  (cached)
?       golang.org/x/text/internal/testtext [no test files]
ok      golang.org/x/text/internal/ucd  (cached)
ok      golang.org/x/text/language  (cached)
ok      golang.org/x/text/unicode/cldr  (cached)
ok      rsc.io/quote    (cached)
ok      rsc.io/sampler  (cached)
$

排除仅适用于当前模块的构建. 如果当前模块被更大的构建所依赖, 则排除不适用. 例如, rsc.io/quotego.mod 中的排除不适用于我们的 "hello, world" 构建.

这一策略的权衡让当前模块的作者几乎可以任意控制自己的构建, 而不会受到它们依赖的模块几乎任意控制的影响.

此时, 正确的下一步是联系 rsc.io/sampler 的作者并在 v1.99.99 中报告问题, 因此它可以在 v1.99.100 中修复. 不幸的是, 作者有一个博文依赖它而不予修复.

替换 (Replacing)

如果确实在依赖中发现了问题, 则需要一种方法将其暂时替换为一个合适的副本. 假设我们想改变一些关于 rsc.io/quote 的行为. 也许我们想要解决 rsc.io/sampler 中的问题, 或者我们想要做其他的事情. 第一步是使用通常的 git 命令检出 quote 模块:

$ git clone https://github.com/rsc/quote ../quote
Cloning into '../quote'...

然后编辑 ../quote/quote.go 来改变 func Hello 的一些内容. 例如, 我把它的返回值从 sampler.Hello() 更改为 sampler.Glass(), 这是一个更有趣的问候语.

$ cd ../quote
$ <edit quote.go>
$

改变了克隆代码之后, 我们可以通过向 go.mod 添加 replace 指令来让我们的构建使用它来代替真正的构建:

replace "rsc.io/quote" v1.5.2 => "../quote"

然后我们可以使用它来构建我们的程序:

$ cd ../hello
$ echo 'replace "rsc.io/quote" v1.5.2 => "../quote"' >>go.mod
$ vgo list -m
MODULE                VERSION
github.com/you/hello  -
golang.org/x/text     v0.0.0-20180208041248-4e4a3210bb54
rsc.io/quote          v1.5.2
 => ../quote
rsc.io/sampler        v1.3.1
$ vgo build
$ ./hello
I can eat glass and it doesn't hurt me.
$

你也可以将一个不同的模块命名为替换模块. 例如, 你可以克隆 github.com/rsc/quote, 然后将更改推送到你自己的分支.

$ cd ../quote
$ git commit -a -m 'my fork'
[master 6151719] my fork
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git tag v0.0.0-myfork
$ git push https://github.com/you/quote v0.0.0-myfork
To https://github.com/you/quote
 * [new tag]         v0.0.0-myfork -> v0.0.0-myfork
$

然后你可以使用它作为替换:

$ cd ../hello
$ echo 'replace "rsc.io/quote" v1.5.2 => "github.com/you/quote" v0.0.0-myfork' >>go.mod
$ vgo list -m
vgo: finding github.com/you/quote v0.0.0-myfork
MODULE                    VERSION
github.com/you/hello      -
golang.org/x/text         v0.0.0-20180208041248-4e4a3210bb54
rsc.io/quote              v1.5.2
 => github.com/you/quote  v0.0.0-myfork
rsc.io/sampler            v1.3.1
$ vgo build
vgo: downloading github.com/you/quote v0.0.0-myfork
$ LANG=fr ./hello
Je peux manger du verre, ça ne me fait pas mal.
$

向后兼容性

即使你想为你的项目使用 vgo, 你也不可能要求你的所有的用户都有 vgo. 相反, 你可以创建一个 vendor 目录, 以允许 go 命令用户生成几乎相同的构建(当然, 在 GOPATH 中编译):

$ vgo vendor
$ mkdir -p $GOPATH/src/github.com/you
$ cp -a . $GOPATH/src/github.com/you/hello
$ go build -o vhello github.com/you/hello
$ LANG=es ./vhello
Puedo comer vidrio, no me hace daño.
$

我说这些构建 "几乎相同", 因为工具链看到的并在最终二进制文件中记录的导入路径是不同的. vendored 版本参见 vendor 目录:

$ go tool nm hello | grep sampler.hello
 1170908 B rsc.io/sampler.hello
$ go tool nm vhello | grep sampler.hello
 11718e8 B github.com/you/hello/vendor/rsc.io/sampler.hello
$

除了这种差异, 构建应该产生相同的二进制文件. 为了提供优雅的转换, 基于 vgo 的构建完全忽略 vendor 目录, 一如既往的模块感知 go 命令构建.

接下来 ?

请尝试 vgo. 在存储库中开始标记(tagging)版本. 创建并检入(check in) go.mod 文件. 在 golang.org/issue 上上报问题, 并在标题开头添加 "x/vgo:" 明天会有更多的博文. 谢谢, 玩得开心!