:::info 日期:2019 年 03 月 19 日
作者:Tyler Bui-Palsulich and Eno Compton
原文链接:https://go.dev/blog/using-go-modules :::

介绍

这篇文章是系列文章的第 1 部分

注意:有关使用模块管理依赖项的文档,请参阅管理依赖项

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:

  1. package hello
  2. func Hello() string {
  3. return "Hello, world."
  4. }

让我们也在 hello_test.go 中编写一个测试:

  1. package hello
  2. import "testing"
  3. func TestHello(t *testing.T) {
  4. want := "Hello, world."
  5. if got := Hello(); got != want {
  6. t.Errorf("Hello() = %q, want %q", got, want)
  7. }
  8. }

此时,该目录包含一个包,但不包含模块,因为没有 go.mod 文件。如果我们在 /home/gopher/hello 中工作并现在运行 go test,我们会看到:

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

最后一行总结了整个包测试。因为我们在 $GOPATH 和任何模块之外工作,所以 go 命令不知道当前目录的导入路径,并根据目录名称创建一个假的:_/home/gopher/hello。

让我们使用 go mod init 将当前目录设为模块的根目录,然后再次尝试 go test

  1. $ go mod init example.com/hello
  2. go: creating new go.mod: module example.com/hello
  3. $ go test
  4. PASS
  5. ok example.com/hello 0.020s
  6. $

恭喜!你已经编写并测试了你的第一个模块。

go mod init 命令写了一个 go.mod 文件:

  1. $ cat go.mod
  2. module example.com/hello
  3. go 1.12
  4. $

go.mod 文件只出现在模块的根目录中。子目录中的包具有由模块路径加上子目录路径组成的导入路径。例如,如果我们创建了一个子目录 world,我们就不需要(也不想)在那里运行 go mod init。该包将被自动识别为 example.com/hello 模块的一部分,导入路径为 example.com/hello/world。

添加依赖

Go 模块的主要动机是改善使用(即添加依赖)其他开发人员编写的代码的体验。

让我们更新我们的 hello.go 以导入 rsc.io/quote 并使用它来实现 Hello:

  1. package hello
  2. import "rsc.io/quote"
  3. func Hello() string {
  4. return quote.Hello()
  5. }

现在让我们再次运行测试

  1. $ go test
  2. go: finding rsc.io/quote v1.5.2
  3. go: downloading rsc.io/quote v1.5.2
  4. go: extracting rsc.io/quote v1.5.2
  5. go: finding rsc.io/sampler v1.3.0
  6. go: finding golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
  7. go: downloading rsc.io/sampler v1.3.0
  8. go: extracting rsc.io/sampler v1.3.0
  9. go: downloading golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
  10. go: extracting golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
  11. PASS
  12. ok example.com/hello 0.023s
  13. $

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文件中只记录了直接依赖:

  1. $ cat go.mod
  2. module example.com/hello
  3. go 1.12
  4. require rsc.io/quote v1.5.2
  5. $

第二个 go test 命令不会重复这项工作,因为 go.mod 现在是最新的并且下载的模块在本地缓存(在 $GOPATH/pkg/mod 中):

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

请注意,虽然 go 命令可以快速轻松地添加新的依赖项,但并非没有代价。您的模块现在实际上依赖于关键领域的新依赖项,例如正确性、安全性和适当的许可,仅举几例。有关更多注意事项,请参阅 Russ Cox 的博客文章“我们的软件依赖问题”。

正如我们在上面看到的,添加一个直接依赖通常也会带来其他间接依赖。命令 go list -m all 列出当前模块及其所有依赖项:

  1. $ go list -m all
  2. example.com/hello
  3. golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
  4. rsc.io/quote v1.5.2
  5. rsc.io/sampler v1.3.0
  6. $

在 go list 输出中,当前模块,也称为主模块,总是在第一行,后面是按模块路径排序的依赖项。

golang.org/x/text 版本 v0.0.0-20170915032832-14c0d48ead0c 是一个伪版本的例子,它是特定未标记提交的 go 命令的版本语法。

除了 go.mod,go 命令还维护一个名为 go.sum 的文件,其中包含特定模块版本内容的预期加密哈希值

  1. $ cat go.sum
  2. golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZO...
  3. golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:Nq...
  4. rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3...
  5. rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPX...
  6. rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/Q...
  7. rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9...
  8. $

go 命令使用 go.sum 文件来确保这些模块的未来下载检索与第一次下载相同的位,以确保您的项目所依赖的模块不会因恶意、意外或其他原因而意外更改。 go.mod 和 go.sum 都应该检查到版本控制中。

升级依赖

使用 Go 模块,版本使用语义版本标签进行引用。语义版本包含三个部分:主要、次要和补丁。比如v0.1.2,大版本是0,小版本是1,补丁版本是2。我们来看看几个小版本的升级。在下一节中,我们将考虑主要版本升级。

从 go list -m all 的输出中,我们可以看到我们使用的是 golang.org/x/text 的未标记版本。让我们升级到最新的标记版本并测试一切是否仍然有效:

  1. $ go get golang.org/x/text
  2. go: finding golang.org/x/text v0.3.0
  3. go: downloading golang.org/x/text v0.3.0
  4. go: extracting golang.org/x/text v0.3.0
  5. $ go test
  6. PASS
  7. ok example.com/hello 0.013s
  8. $

呜呼!一切都会过去。让我们再看看 go list -m all 和 go.mod 文件:

  1. $ go list -m all
  2. example.com/hello
  3. golang.org/x/text v0.3.0
  4. rsc.io/quote v1.5.2
  5. rsc.io/sampler v1.3.0
  6. $ cat go.mod
  7. module example.com/hello
  8. go 1.12
  9. require (
  10. golang.org/x/text v0.3.0 // indirect
  11. rsc.io/quote v1.5.2
  12. )
  13. $

golang.org/x/text 包已升级到最新的标记版本 (v0.3.0)。 go.mod 文件也已更新为指定 v0.3.0。间接注释表示该模块不直接使用某个依赖项,而是其他模块依赖项间接使用该依赖项。有关详细信息,请参阅 go 帮助模块。

现在让我们尝试升级 rsc.io/sampler 次要版本。以同样的方式开始,通过运行 go get 并运行测试:

  1. $ go get rsc.io/sampler
  2. go: finding rsc.io/sampler v1.99.99
  3. go: downloading rsc.io/sampler v1.99.99
  4. go: extracting rsc.io/sampler v1.99.99
  5. $ go test
  6. --- FAIL: TestHello (0.00s)
  7. hello_test.go:8: Hello() = "99 bottles of beer on the wall, 99 bottles of beer, ...", want "Hello, world."
  8. FAIL
  9. exit status 1
  10. FAIL example.com/hello 0.014s
  11. $

哦,哦!测试失败说明最新版本的rsc.io/sampler与我们的使用不兼容。让我们列出该模块的可用标记版本:

  1. $ go list -m -versions rsc.io/sampler
  2. rsc.io/sampler v1.0.0 v1.2.0 v1.2.1 v1.3.0 v1.3.1 v1.99.99
  3. $

我们一直在使用 v1.3.0; v1.99.99 显然不好。也许我们可以尝试使用 v1.3.1 代替:

  1. $ go get rsc.io/sampler@v1.3.1
  2. go: finding rsc.io/sampler v1.3.1
  3. go: downloading rsc.io/sampler v1.3.1
  4. go: extracting rsc.io/sampler v1.3.1
  5. $ go test
  6. PASS
  7. ok example.com/hello 0.022s
  8. $

请注意 go get 参数中的显式 @v1.3.1。通常,传递给 go get 的每个参数都可以采用显式版本;默认值为@latest,它解析为之前定义的最新版本。

添加对新主要版本的依赖

让我们向我们的包中添加一个新函数:func Proverb 通过调用由模块 rsc.io/quote/v3 提供的 quote.Concurrency 返回一个 Go 并发谚语。首先我们更新 hello.go 以添加新函数:

  1. package hello
  2. import (
  3. "rsc.io/quote"
  4. quoteV3 "rsc.io/quote/v3"
  5. )
  6. func Hello() string {
  7. return quote.Hello()
  8. }
  9. func Proverb() string {
  10. return quoteV3.Concurrency()
  11. }

然后我们在 hello_test.go 中添加一个测试:

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

然后我们可以测试我们的代码:

  1. $ go test
  2. go: finding rsc.io/quote/v3 v3.1.0
  3. go: downloading rsc.io/quote/v3 v3.1.0
  4. go: extracting rsc.io/quote/v3 v3.1.0
  5. PASS
  6. ok example.com/hello 0.024s
  7. $

请注意,我们的模块现在依赖于 rsc.io/quote 和 rsc.io/quote/v3:

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

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:

  1. $ go doc rsc.io/quote/v3
  2. package quote // import "rsc.io/quote/v3"
  3. Package quote collects pithy sayings.
  4. func Concurrency() string
  5. func GlassV3() string
  6. func GoV3() string
  7. func HelloV3() string
  8. func OptV3() string
  9. $

我们可以更新我们在 hello.go 中使用 quote.Hello() 以使用 quoteV3.HelloV3() :

  1. package hello
  2. import quoteV3 "rsc.io/quote/v3"
  3. func Hello() string {
  4. return quoteV3.HelloV3()
  5. }
  6. func Proverb() string {
  7. return quoteV3.Concurrency()
  8. }

然后此时,不再需要重命名的导入,因此我们可以撤消它:

  1. package hello
  2. import "rsc.io/quote/v3"
  3. func Hello() string {
  4. return quote.HelloV3()
  5. }
  6. func Proverb() string {
  7. return quote.Concurrency()
  8. }

让我们重新运行测试以确保一切正常:

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

删除未使用的依赖项

我们已经删除了对 rsc.io/quote 的所有使用,但它仍然出现在 go list -m all 和我们的 go.mod 文件中:

  1. $ go list -m all
  2. example.com/hello
  3. golang.org/x/text v0.3.0
  4. rsc.io/quote v1.5.2
  5. rsc.io/quote/v3 v3.1.0
  6. rsc.io/sampler v1.3.1
  7. $ cat go.mod
  8. module example.com/hello
  9. go 1.12
  10. require (
  11. golang.org/x/text v0.3.0 // indirect
  12. rsc.io/quote v1.5.2
  13. rsc.io/quote/v3 v3.0.0
  14. rsc.io/sampler v1.3.1 // indirect
  15. )
  16. $

为什么?因为构建单个包,例如使用 go build 或 go test,可以轻松判断何时缺少某些内容并需要添加,但无法确定何时可以安全删除某些内容。只有在检查模块中的所有包以及这些包的所有可能的构建标记组合后,才能删除依赖项。普通的构建命令不会加载这些信息,因此它不能安全地删除依赖项。

go mod tidy 命令会清理这些未使用的依赖项:

  1. $ go mod tidy
  2. $ go list -m all
  3. example.com/hello
  4. golang.org/x/text v0.3.0
  5. rsc.io/quote/v3 v3.1.0
  6. rsc.io/sampler v1.3.1
  7. $ cat go.mod
  8. module example.com/hello
  9. go 1.12
  10. require (
  11. golang.org/x/text v0.3.0 // indirect
  12. rsc.io/quote/v3 v3.1.0
  13. rsc.io/sampler v1.3.1 // indirect
  14. )
  15. $ go test
  16. PASS
  17. ok example.com/hello 0.020s
  18. $

总结

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 依赖管理的未来,请向我们发送错误报告体验报告

感谢您的所有反馈并帮助改进模块。