本文译自
A Tour of Versioned Go (vgo) ,
Go & Versioning 的第 2 部分, 版权@归原文所有.
对我而言, 设计意味着一遍又一遍地构建, 拆除和再构建. 为了编写新的版本控制提案 , 我构建了一个原型 vgo, 来处理许多细微的细节. 这篇博文展示了如何使用 vgo.
你现在可以通过运行 go get golang.org/x/vgo 下载并尝试 vgo. Vgo 是 go 命令的一个直接替换(和分支拷贝). 你运行 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 ( "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.3 或 v1.6.0, 该目录中的构建仍将继续使用 v1.5.2, 除非进行明确的升级(见下文).
go.mod 文件列举了依赖的最小集合, 忽略了已列举中所隐含的. 在这种情况下, rsc.io/quote v1.5.2 依赖特定版本的 rsc.io/sampler 和 golang.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/text 和 rsc.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/quote 和 rsc.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/quote 的 go.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 pu 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:” 明天会有更多的博文. 谢谢, 玩得开心!