Go 博客
使用 Go Modules
引言
本文是系列文章的第 1 部分。
- 第 1 部分 — 使用 Go Modules (本文)
- 第 2 部分 — 迁移到 Go Modules
- 第 3 部分 — 发布 Go Modules
- 第 4 部分 — Go Modules:v2 及更高版本
- 第 5 部分 — 保持您的模块兼容
注意:有关使用模块管理依赖的文档,请参阅管理依赖。
Go 1.11 和 1.12 包含了对模块的初步支持,这是 Go 新的依赖管理系统,它使依赖版本信息更明确且易于管理。这篇博文介绍了开始使用模块所需的基本操作。
模块是存储在文件树中的Go 包的集合,根目录下包含一个 go.mod
文件。go.mod
文件定义了模块的模块路径(它也是根目录的导入路径)及其依赖要求(成功构建所需的其他模块)。每个依赖要求都写为模块路径和一个特定的语义版本。
从 Go 1.11 开始,当当前目录或任何父目录包含 go.mod
文件时,go 命令启用模块的使用,前提是该目录不在 $GOPATH/src
内。(为了兼容性,在 $GOPATH/src
内,即使找到了 go.mod
文件,go 命令仍会以旧的 GOPATH 模式运行。详情请参阅go 命令文档。) 从 Go 1.13 开始,模块模式将成为所有开发的默认设置。
本文将介绍使用模块开发 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
命令不知道当前目录的导入路径,因此根据目录名生成了一个假的导入路径:_/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
。
添加依赖
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
中列出的特定依赖模块版本来解析导入。当遇到 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
都应该纳入版本控制。
升级依赖
使用 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
通过调用 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 模块的每个不同主要版本(v1
、v2
等)都使用不同的模块路径:从 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
向后兼容,但错误或客户端对模块行为的不正确假设都可能发生。)
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
的使用。增量迁移的能力在大程序或代码库中尤为重要。
将依赖升级到新的主要版本
让我们完成从使用 rsc.io/quote
到仅使用 rsc.io/quote/v3
的转换。由于主要版本发生了变化,我们应该预期一些 API 可能已被移除、重命名或以不兼容的方式更改。阅读文档,我们可以看到 Hello
已变为 HelloV3
:
$ go doc rsc.io/quote/v3
package quote // import "rsc.io/quote/v3"
Package quote collects pithy sayings.
func Concurrency() string
func GlassV3() string
func GoV3() string
func HelloV3() string
func OptV3() string
$
我们可以更新 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()
}
此时,不再需要重命名的导入,因此我们可以撤销它:
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
移除未使用的依赖
我们已经移除了所有对 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 版本中可用(即在 Go 1.11 和 Go 1.12 中)。
本文介绍了使用 Go modules 的以下工作流程:
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 依赖管理的未来,请向我们发送错误报告或经验报告。
感谢您的所有反馈和帮助改进模块。
下一篇文章:调试 Go 1.12 中部署的内容
上一篇文章:新的 Go 开发者网络
博客索引