本文译自 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 "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.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/[email protected]
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/[email protected]
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/[email protected]
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 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:" 明天会有更多的博文. 谢谢, 玩得开心!