原文 Using Go Modules(尚未发布)
介绍
Go 1.11和1.12包含了初步的modules支持,Go的新版本管理系统用于依赖版本信息描述和更方便的管理。这篇博客是一个关于开始使用modules的基础操作指引教程。后续文章会介绍发布一个其他人可以使用的modules。
modules是Go包的集合,保存在顶层目录一个名叫go.mod
的文件中。go.mod
文件定义了模块的路径,这个会作为根目录引用路径;同时文件中也包含了能够正常构建的其他包依赖需求。每个依赖需求同样以模块路径方式标示,同时根据语义化版本方式进行标记。
在Go 1.11开始,go命令行会再当前目录或者上层目录中存在go.mod
文件并且在 $GOPATH/src
目录外时自动启用modules功能。(当目录位于$GOPATH/src
中时,出于兼容性考虑,go命令仍旧采用GOPATH模式,即便存在go.mod
文件。具体请参考Go命令行文档)。从Go
1.13版本开始,modules功能将会在所有开发过程中默认开启。
这篇博客会演示使用modules开发Go代码的一系列的常用操作:
- 创建一个模块
- 添加依赖
- 升级依赖
- 添加一个依赖的新主版本
- 升级一个依赖到新主版本
- 移除无用依赖
创建一个新的模块
让我门从创建一个新模块开始。
在$GOPATH/src
外创建一个新的空文件夹,使用cd
切换进入这个目录,然后创建一个名叫hello.go
的新源码文件:
package hello
func Hello() string {
return "Hello, world."
}
让我们同样创建一个名叫hello_test.go
的测试文件:
package hello
import "testing"
func TestHello(t *testing.T) {
want := "Hello, world."
if got := Hello(); got != want {
t.Errorf("Hello() = %q, want %q", got, want)
}
}
现在,这个目录包含了一个包,但是它并不是一个模块,因为还没有go.mod
文件。如果你文件创建在 /home/gopher/hello
目录下,执行go test
命令时,我们可以看到结果:
$ go test
PASS
ok _/home/gopher/hello 0.020s
$
最后一行表示了整个包测试的汇总信息。因为我们现在在 $GOPATH
外,并且不属于任何模块,因此 go
命令不知道当前目录的引用路径,因此采用当前文件夹名生成了一个伪路径。
现在让我们使用go mod init
命令将当前目录设置成为模块的根目录,然后重新试试go test
命令:
$ go mod init example.com/hello
go: creating new go.mod: module example.com/hello
$ go test
PASS
ok example.com/hello 0.020s
$
恭喜你!你已经编写和测试了你的第一个模块。
go mod init
命令会创建一个go.mod
文件:
$ cat go.mod
module example.com/hello
go 1.12
$
go.mod
文件仅出现在模块的根目录下。子目录中的包引用路径会使用模块的引用路径加上子目录路径的形式。举个例子,如果我们创建了一个名叫world
的子目录,我们不需要在子目录中使用go mod init
命令。包会自动识别作为example.com/hello
中的一部分,引用路径为example.com/hello/world
。
添加一个依赖
Go modules 功能的主要动机就是提升使用其他开发者代码(或者说添加一个依赖项)时的体验。
让我们更新一下hello.go
,引入rsc.io/quote
并且使用它实现Hello
:
package hello
import "rsc.io/quote"
func Hello() string {
return quote.Hello()
}
现在让我们再次执行测试:
$ go test
go: finding rsc.io/quote v1.5.2
go: downloading rsc.io/quote v1.5.2
go: extracting rsc.io/quote v1.5.2
go: finding rsc.io/sampler v1.3.0
go: finding golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
go: downloading rsc.io/sampler v1.3.0
go: extracting rsc.io/sampler v1.3.0
go: downloading golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
go: extracting golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
PASS
ok example.com/hello 0.023s
$
go
命令会自动处理go.mod
中制定的依赖版本。当包中import
指向的模块没有在 go.mod
文件中, go
命令会自动搜索这个模块,并且将最新版本添加到 go.mod
文件中。(“最新版本”是指最新的标签非预发布的稳定版本。)在我们的例子中,go test
解析了新的引用路径rsc.io/quote
为对应的模块,版本为v1.5.2
。它同样下载了rsc.io/quote
中使用的两个依赖,名为rsc.io/sampler
和golang.org/x/text
。不过,只有直接使用的依赖会被记录在go.mod
文件中。
$ cat go.mod
module example.com/hello
go 1.12
require rsc.io/quote v1.5.2
$
第二次执行go test
命令时不会重复这个工作,因为 go.mod
已经最新并且所有的下载模块都会缓存到本地(保存在$GOPATH/pkg/mod
目录中):
$ go test
PASS
ok example.com/hello 0.020s
$
值得注意的是,虽然go
命令添加新依赖简单便捷,但是它并不是没有成本的。你的模块递归依赖新的依赖时可能会有正确性、安全性和非正当授权等等问题。更多的思考,可以参考Russ Cox的博客我们的软件依赖问题。
如同我们上面看到的那样,添加一新的依赖通常会带来新的间接依赖。命令go list -m all
会列出虽有的当前依赖和他们的依赖
$ go list -m all
example.com/hello
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0
$
在go list
输出中,当前模块,或者叫做主模块,通常是第一行,接下来是根据依赖路径排序的依赖。
golang.org/x/text
的版本v0.0.0-20170915032832-14c0d48ead0c
是伪版本的例子,是go
命令的版本语法用于标记未打标签的提交。
同时,go.mod
和go
命令维护了一个名叫go.sum
的文件包含了指定模块版本的期望的加密hash:
$ cat go.sum
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZO...
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:Nq...
rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3...
rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPX...
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/Q...
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9...
$
go
命令使用go.sum
文件保证之后的模块下载会下载到跟第一次下载相同的文件内容,保证你的项目依赖不会发生预期外的恶意修改、意外问题和其他问题。go.mod
和 go.sum
都需要放进版本管理中。
升级依赖
在Go modules中,版本采用语义化版本标签标记版本。语义化版本包含三部分:主要版本、次要版本和修订版本。举一个例子,比如,v0.1.2
,主要版本为0,次要版本为1,修订版本为2.
让我们在这一节中先升级一下次要版本,下一节中我们再尝试升级一下主要版本。
从go list -m all
的输出中,我们可以看到我们使用了 golang.org/x/text
的一个没有标签的版本,让我们吧这个版本升级成最新版本,然后测试一下所有的功能是否正常工作:
$ go get golang.org/x/text
go: finding golang.org/x/text v0.3.0
go: downloading golang.org/x/text v0.3.0
go: extracting golang.org/x/text v0.3.0
$ go test
PASS
ok example.com/hello 0.013s
$
哇哦,所有的功能都正常工作。
让我们看一下go list -m all
和go.mod
文件:
$ go list -m all
example.com/hello
golang.org/x/text v0.3.0
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0
$ cat go.mod
module example.com/hello
go 1.12
require (
golang.org/x/text v0.3.0 // indirect
rsc.io/quote v1.5.2
)
$
golang.org/x/text
文件被更新到了最新的标签版本v0.3.0
。 go.mod
文件中的记录也被更新成了v0.3.0
。indirect
注释标记了依赖不是被当前模块直接使用的,只是在其他依赖项中被间接引用。具体内容可以通过go help modules
查看更多介绍。
现在,我们可以尝试升级rsc.io/sampler
的小版本,同样的,我们使用go get
指令后执行测试:
$ go get rsc.io/sampler
go: finding rsc.io/sampler v1.99.99
go: downloading rsc.io/sampler v1.99.99
go: extracting rsc.io/sampler v1.99.99
$ go test
--- FAIL: TestHello (0.00s)
hello_test.go:8: Hello() = "99 bottles of beer on the wall, 99 bottles of beer, ...", want "Hello, world."
FAIL
exit status 1
FAIL example.com/hello 0.014s
$
噢,不!测试显示最新版的 rsc.io/sampler
不兼容我们的使用方式,让我们看一下这个模块所有可用的标签版本:
$ go list -m -versions rsc.io/sampler
rsc.io/sampler v1.0.0 v1.2.0 v1.2.1 v1.3.0 v1.3.1 v1.99.99
$
我们在使用的版本是v1.3.0,看上去v1.99.99明显不能使用,或许我们可以使用v1.3.1版本:
$ go get rsc.io/sampler@v1.3.1
go: finding rsc.io/sampler v1.3.1
go: downloading rsc.io/sampler v1.3.1
go: extracting rsc.io/sampler v1.3.1
$ go test
PASS
ok example.com/hello 0.022s
$
注意go get
命令中我们使用了`@v1.3.1作为参数。通常情况下
go get可以指定参数标记使用的版本,这个默认值为
@latest`,对应会尝试使用最新的版本。
添加一个依赖的主要版本
让我们添加一个新的函数到我们的包中:func Proverb
会返回Go的并发箴言,这个功能是在rsc.io/quote/v3
模块中通过调用 quote.Concurrency
实现的。
现在,我们来更新hello.go
添加新的函数:
package hello
import (
"rsc.io/quote"
quoteV3 "rsc.io/quote/v3"
)
func Hello() string {
return quote.Hello()
}
func Proverb() string {
return quoteV3.Concurrency()
}
现在,我们来添加对应的测试:
func TestProverb(t *testing.T) {
want := "Concurrency is not parallelism."
if got := Proverb(); got != want {
t.Errorf("Proverb() = %q, want %q", got, want)
}
}
现在让我们测试一下:
$ go test
go: finding rsc.io/quote/v3 v3.1.0
go: downloading rsc.io/quote/v3 v3.1.0
go: extracting rsc.io/quote/v3 v3.1.0
PASS
ok example.com/hello 0.024s
$
注意,我们同时使用了rsc.io/quote
和rsc.io/quote/v3
:
$ go list -m rsc.io/q...
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0
$
每个不同主要版本(v1
, v2
等等)可以使用不用的引用路径:路径采用主要版本结尾。在这个例子中,我们使用的rsc.io/quote
的v3
版本不再是rsc.io/quote
,而是rsc.io/quote/v3
。这个叫做语义化引用版本,可以让不兼容的包(通常是不同主版本)拥有不同的路径。在差异中rsc.io/quote
的v1.6.0
版本需要向前兼容v1.5.2
,这样就可以重用rsc.io/quote
名称。(在前一节中,
rsc.io/sampler
的v1.99.99
应该需要兼容rsc.io/sampler
的v1.3.0
版本,但是Bug或者其他原因都可能导致这种问题的出现。)
go
命令允许一个构建包含一个指定模块路径的最多一个版本,以为着每个主要版本都可以包含最多一个版本:一个rsc.io/quote
,一个rsc.io/quote/v2
和一个rsc.io/quote/v3
等等。
这规定了模块作者一个清晰的单一模块路径的规则:可以一个程序既可以在rsc.io/quote
的v1.5.2
和v1.6.0
版本下构建通过。同时,允许不同的模块主版本(因为有不同路径)让模块的消费者能够增量升级至新主版本中。在这个例子中,我们希望使用rsc/quote/v3 v3.1.0
中的quote.Concurrency
,但是我们没做好合并我们rsc.io/quote v1.5.2
的使用方式时,这个能力可以帮助到我们。这个能力在大型代码的项目中的增量更新中尤为重要。
升级一个依赖到新主版本
现在,让我们完成将使用rsc.io/quote
转换为仅使用rsc.io/quote/v3
。因为主版本的变化,我们可以预计到一些API已经删除,改名或者发生了一些不兼容的变化。阅读一下文档我们可以知道,Hello
现在已经变为了HelloV3
:
$ go doc rsc.io/quote/v3
package quote // import "rsc.io/quote"
Package quote collects pithy sayings.
func Concurrency() string
func GlassV3() string
func GoV3() string
func HelloV3() string
func OptV3() string
$
(这里输出中有一个已知BUG,显示内容中引用路径错误的丢弃了/v3
。)
我们可以更新我们hello.go
文件中的quote.Hello()
为quoteV3.Hello()
:
package hello
import quoteV3 "rsc.io/quote/v3"
func Hello() string {
return quoteV3.Hello()
}
func Proverb() string {
return quoteV3.Concurrency()
}
这时,我们已经不再需要重命名引用了,我们可以移除掉这些:
package hello
import "rsc.io/quote/v3"
func Hello() string {
return quote.Hello()
}
func Proverb() string {
return quote.Concurrency()
}
现在我们重新运行一下测试,确保所有的功能都能够正常运行。
$ go test
PASS
ok example.com/hello 0.014s
移除无用依赖
我们已经移除了所有rsc.io/quote
的使用,但是当我们执行go list -m all
命令时,我们仍旧可以看到它出现在我们的go.mod
文件中。
$ go list -m all
example.com/hello
golang.org/x/text v0.3.0
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0
rsc.io/sampler v1.3.1
$ cat go.mod
module example.com/hello
go 1.12
require (
golang.org/x/text v0.3.0 // indirect
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.0.0
rsc.io/sampler v1.3.1 // indirect
)
$
为什么呢?因为当我们构建一个单独的包时,如执行go build
或者go test
时,很容易得知哪些包遗失了或者需要被添加,但是很难确认哪些包可以被安全移除。只有在检查完模块所有的包和可能的构建标签组合之后,才能移除一个依赖。普通的构建命令不会夹在这些信息,因此无法安全的移除依赖。
go mod tidy
命令则可以帮助清理无用依赖:
$ go mod tidy
$ go list -m all
example.com/hello
golang.org/x/text v0.3.0
rsc.io/quote/v3 v3.1.0
rsc.io/sampler v1.3.1
$ cat go.mod
module example.com/hello
go 1.12
require (
golang.org/x/text v0.3.0 // indirect
rsc.io/quote/v3 v3.1.0
rsc.io/sampler v1.3.1 // indirect
)
$ go test
PASS
ok example.com/hello 0.020s
$
总结
Go modules功能是Go中依赖管理的未来。模块功能现在已经在目前受技术支持的Go版本中可用(目前是Go1.11和Go1.12)。
这篇文章介绍了使用Go modules的一些工作流:
go mod init
创建一个新的模块,初始化go.mod
文件。go build
和go test
命令和其他一些包构建命令添加必要新依赖到go.mod
文件。go list -m all
打印当前模块依赖。go get
命令修改依赖的版本(或者新增依赖)。go mod tidy
移除无用依赖。
我们鼓励你从现在开始在你的本地开发中启用modules功能,并且添加go.mod
和 go.sum
文件到项目中。你可以给我们发送BUG反馈或者是体验报告,反馈和帮助Go依赖管理的未来演进。
感谢你们的反馈帮助提升modules功能。