使用Go Modules

原文:Using Go Modules
作者:Tyler Bui-Palsulich、Eno Compton

介绍

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的新源码文件:

1
2
3
4
5
package hello

func Hello() string {
return "Hello, world."
}

让我们同样创建一个名叫hello_test.go的测试文件:

1
2
3
4
5
6
7
8
9
10
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命令时,我们可以看到结果:

1
2
3
4
$ go test
PASS
ok _/home/gopher/hello 0.020s
$

最后一行表示了整个包测试的汇总信息。因为我们现在在 $GOPATH外,并且不属于任何模块,因此 go命令不知道当前目录的引用路径,因此采用当前文件夹名生成了一个伪路径。

现在让我们使用go mod init命令将当前目录设置成为模块的根目录,然后重新试试go test命令:

1
2
3
4
5
6
$ 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文件:

1
2
3
4
5
$ 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

1
2
3
4
5
6
7
package hello

import "rsc.io/quote"

func Hello() string {
return quote.Hello()
}

现在让我们再次执行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ 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/samplergolang.org/x/text。不过,只有直接使用的依赖会被记录在go.mod文件中。

1
2
3
4
5
6
7
$ cat go.mod
module example.com/hello

go 1.12

require rsc.io/quote v1.5.2
$

第二次执行go test命令时不会重复这个工作,因为 go.mod 已经最新并且所有的下载模块都会缓存到本地(保存在$GOPATH/pkg/mod目录中):

1
2
3
4
$ go test
PASS
ok example.com/hello 0.020s
$

值得注意的是,虽然go命令添加新依赖简单便捷,但是它并不是没有成本的。你的模块递归依赖新的依赖时可能会有正确性、安全性和非正当授权等等问题。更多的思考,可以参考Russ Cox的博客我们的软件依赖问题

如同我们上面看到的那样,添加一新的依赖通常会带来新的间接依赖。命令go list -m all会列出虽有的当前依赖和他们的依赖

1
2
3
4
5
6
$ 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.modgo命令维护了一个名叫go.sum的文件包含了指定模块版本的期望的加密hash

1
2
3
4
5
6
7
8
$ 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.modgo.sum都需要放进版本管理中。

升级依赖

在Go modules中,版本采用语义化版本标签标记版本。语义化版本包含三部分:主要版本、次要版本和修订版本。举一个例子,比如,v0.1.2,主要版本为0,次要版本为1,修订版本为2.

让我们在这一节中先升级一下次要版本,下一节中我们再尝试升级一下主要版本。

go list -m all的输出中,我们可以看到我们使用了 golang.org/x/text的一个没有标签的版本,让我们吧这个版本升级成最新版本,然后测试一下所有的功能是否正常工作:

1
2
3
4
5
6
7
8
$ 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 allgo.mod文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ 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.0go.mod文件中的记录也被更新成了v0.3.0indirect 注释标记了依赖不是被当前模块直接使用的,只是在其他依赖项中被间接引用。具体内容可以通过go help modules查看更多介绍。

现在,我们可以尝试升级rsc.io/sampler的小版本,同样的,我们使用go get指令后执行测试:

1
2
3
4
5
6
7
8
9
10
11
$ 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不兼容我们的使用方式,让我们看一下这个模块所有可用的标签版本:

1
2
3
$ 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版本:

1
2
3
4
5
6
7
8
$ 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 添加新的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package hello

import (
"rsc.io/quote"
quoteV3 "rsc.io/quote/v3"
)

func Hello() string {
return quote.Hello()
}

func Proverb() string {
return quoteV3.Concurrency()
}

现在,我们来添加对应的测试:

1
2
3
4
5
6
func TestProverb(t *testing.T) {
want := "Concurrency is not parallelism."
if got := Proverb(); got != want {
t.Errorf("Proverb() = %q, want %q", got, want)
}
}

现在让我们测试一下:

1
2
3
4
5
6
7
$ 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/quotersc.io/quote/v3

1
2
3
4
$ go list -m rsc.io/q...
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0
$

每个不同主要版本(v1, v2等等)可以使用不用的引用路径:路径采用主要版本结尾。在这个例子中,我们使用的rsc.io/quotev3 版本不再是rsc.io/quote,而是rsc.io/quote/v3。这个叫做语义化引用版本,可以让不兼容的包(通常是不同主版本)拥有不同的路径。在差异中rsc.io/quotev1.6.0版本需要向前兼容v1.5.2,这样就可以重用rsc.io/quote名称。(在前一节中, rsc.io/samplerv1.99.99应该需要兼容rsc.io/samplerv1.3.0版本,但是Bug或者其他原因都可能导致这种问题的出现。)

go命令允许一个构建包含一个指定模块路径的最多一个版本,以为着每个主要版本都可以包含最多一个版本:一个rsc.io/quote,一个rsc.io/quote/v2和一个rsc.io/quote/v3等等。

这规定了模块作者一个清晰的单一模块路径的规则:可以一个程序既可以在rsc.io/quotev1.5.2v1.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

1
2
3
4
5
6
7
8
9
10
11
$ 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()

1
2
3
4
5
6
7
8
9
10
11
package hello

import quoteV3 "rsc.io/quote/v3"

func Hello() string {
return quoteV3.Hello()
}

func Proverb() string {
return quoteV3.Concurrency()
}

这时,我们已经不再需要重命名引用了,我们可以移除掉这些:

1
2
3
4
5
6
7
8
9
10
11
package hello

import "rsc.io/quote/v3"

func Hello() string {
return quote.Hello()
}

func Proverb() string {
return quote.Concurrency()
}

现在我们重新运行一下测试,确保所有的功能都能够正常运行。

1
2
3
$ go test
PASS
ok example.com/hello 0.014s

移除无用依赖

我们已经移除了所有rsc.io/quote的使用,但是当我们执行go list -m all命令时,我们仍旧可以看到它出现在我们的go.mod文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ 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命令则可以帮助清理无用依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ 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 buildgo test命令和其他一些包构建命令添加必要新依赖到go.mod文件。
  • go list -m all打印当前模块依赖。
  • go get命令修改依赖的版本(或者新增依赖)。
  • go mod tidy移除无用依赖。

我们鼓励你从现在开始在你的本地开发中启用modules功能,并且添加go.modgo.sum文件到项目中。你可以给我们发送BUG反馈或者是体验报告,反馈和帮助Go依赖管理的未来演进。

感谢你们的反馈帮助提升modules功能。