Go Modules
翻译自 Using Go Modules
Go 1.11 和 1.12 包括对模块的初步支持,Go的新依赖管理系统使依赖版本信息明确且易于管理。此博客文章是介绍开始使用模块所需的基本操作的教程。后续帖子将涵盖发布供其他人使用的模块。
module 是存储在文件树中的Go包的集合,其根目录中包含 go.mod
文件。go.mod
文件定义了模块的 module path
,它也是用于根目录的导入路径,以及它的依赖性要求,它们是成功构建所需的其他模块。每个依赖性要求都被写为模块路径和特定语义版本。
从Go 1.11开始,go命令允许在当前目录或任何父目录具有 go.mod
时使用模块,前提是目录在 $GOPATH/src
之外。(在 $GOPATH/src
中,为了兼容性,go命令仍然在旧的GOPATH模式下运行,即使找到了 go.mod
也是如此。详情请参阅 go命令文档)。从Go 1.13开始,模块模式将是所有开发的默认模式。
本文将介绍使用模块开发Go代码时出现的一系列常见操作:
- Creating a new module. 创建新模块
- Adding a dependency. 添加依赖项
- Upgrading dependencies. 升级依赖项
- Adding a dependency on a new major version. 添加新版本的依赖项
- Upgrading a dependency to a new major version. 升级依赖项到新版本
- Removing unused dependencies. 删除未使用的依赖项
Creating a new module
创建一个新模块。
在 $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命令不知道当前目录的导入路径,而是根据目录名称构成假路径:_/home/gopher/hello
。
让我们使用 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
。
Adding a dependency
Go模块的主要动机是改善使用(即添加依赖性)其他开发人员编写的代码的体验。
让我们更新我们的 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 中列出的特定依赖模块版本来解析导入。当遇到 go.mod 中任何模块都未提供的软件包导入时,go命令会自动查找包含该软件包的模块,并使用最新版本将其添加到go.mod中(“最新”被定义为最新的标记稳定版(非预发行版),或者最新的标记预发布版本,或者最新的未标记版本)。在我们的示例中,go test 将新导入 rsc.io/quote
解析为模块 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
的文件,其中包含特定模块版本内容的预期加密哈希:
$ 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都应检入版本控制。
Upgrading dependencies
使用Go模块,版本使用语义版本标记引用。语义版本包含三个部分:major,minor和patch。例如,对于 v0.1.2
,主要版本 major 为0,次要版本 minor 为1,修补程序版本 patch 为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 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
,它解析为之前定义的最新版本。
Adding a dependency on a new major version
让我们为我们的包添加一个新函数:func Proverb
通过调用 quote.Concurrency
返回一个Go并发谚语,该函数由模块 rsc.io/quote/v3
提供。首先我们更新 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()
}
然后我们在 hello_test.go
中添加一个测试:
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
Go模块的每个不同的 major 版本(v1,v2等)使用不同的模块路径:从v2开始,路径必须以 major 版本结束。 在该示例中,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向后兼容,但是有关模块行为的错误或不正确的客户端假设都可能发生。)
go命令允许构建包含任何特定模块路径的最多一个版本,最多意味着每个主要版本之一:一个 rsc.io/quote
,一个 rsc.io/quote/v2
,一个 rsc.io/quote/ v3
,依此类推。 这为模块作者提供了关于单个模块路径可能重复的明确规则:使用 rsc.io/quote v1.5.2
和 rsc.io/quote v1.6.0
构建程序是不可能的。 同时,允许模块的不同主要版本(因为它们具有不同的路径)使模块使用者能够以递增方式升级到新的主要版本。 在这个例子中,我们想使用 rsc/quote/v3
v3.1.0中的 quote.Concurrency
但尚未准备好迁移我们对 rsc.io/quote v1.5.2
的使用。 在大型程序或代码库中,逐步迁移的能力尤为重要。
Upgrading a dependency to a new major version
让我们完成从使用 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
(输出中还有一个已知错误:显示的导入路径错误地删除了 /v3
)。
我们可以更新 hello.go 中 quote.Hello()
的使用,以使用 quoteV3.HelloV3()
:
package hello
import quoteV3 "rsc.io/quote/v3"
func Hello() string {
return quoteV3.HelloV3()
}
func Proverb() string {
return quoteV3.Concurrency()
}
然后在这一点上,不再需要重命名 import 的名称,所以我们可以撤消它:
package hello
import "rsc.io/quote/v3"
func Hello() string {
return quote.HelloV3()
}
func Proverb() string {
return quote.Concurrency()
}
让我们重新运行测试以确保一切正常:
$ go test
PASS
ok example.com/hello 0.014s
Removing unused dependencies
我们已经删除了对 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
Conclusion
Go模块是Go中依赖管理的未来。 现在,所有支持的Go版本(即Go 1.11和Go 1.12)都提供了模块功能。
这篇文章使用Go模块介绍了这些工作流程:
go mod init
创建一个新模块,初始化描述它的go.mod
文件。go build
,go test
和其他包构建命令根据需要为go.mod
添加新的依赖项。go list -m all
打印当前模块的依赖项。go get
更改所需的依赖项的版本(或添加新的依赖项)。go mod tidy
删除未使用的依赖项。
我们鼓励您开始在本地开发中使用模块,并将 go.mod 和 go.sum 文件添加到项目中。 为了提供反馈并帮助塑造Go中依赖管理的未来,请向我们发送错误报告或体验报告。
感谢您的所有反馈和帮助改进模块。
By Tyler Bui-Palsulich and Eno Compton