Go 博客

使用 Go 模块

Tyler Bui-Palsulich 和 Eno Compton
2019 年 3 月 19 日

介绍

这篇文章是一个系列文章的第一部分。

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

Go 1.11 和 1.12 包括对模块的初步 支持,Go 的 新的依赖项管理系统 使依赖项版本信息更加明确,易于管理。这篇博文介绍了开始使用模块所需的常用操作。

模块是一组 Go 包,存储在文件树中,根目录下有一个 go.mod 文件。go.mod 文件定义了模块的模块路径,这也是根目录的导入路径,以及它的依赖项要求,即成功构建所需的其他模块。每个依赖项要求都以模块路径和特定 语义版本 形式编写。

从 Go 1.11 开始,只要当前目录或任何父目录包含 go.mod,并且该目录位于 $GOPATH/src 之外,go 命令就会启用模块的使用。(在 $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 模块的主要动机是改善使用(即添加对其他开发人员编写的代码的依赖项)的体验。

让我们更新 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 中任何模块未提供的包的 import 时,go 命令会自动查找包含该包的模块,并将其添加到 go.mod 中,使用最新版本。(“最新”是指最新的标记为稳定(非 预发行)版本,或者最新的标记为预发行版本,或者最新的未标记版本。)在我们的示例中,go test 将新的导入 rsc.io/quote 解析为模块 rsc.io/quote v1.5.2。它还下载了 rsc.io/quote 使用的两个依赖项,即 rsc.io/samplergolang.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.modgo.sum 都应该签入版本控制。

升级依赖项

使用 Go 模块,版本使用语义版本标签引用。语义版本包含三个部分:主版本、次版本和修订版本。例如,对于 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 allgo.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.0indirect 注释表示依赖项不是由本模块直接使用,而是由其他模块依赖项间接使用。有关详细信息,请参阅 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/[email protected]
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 并发谚语,通过调用 quote.Concurrency,该函数由模块 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/quotersc.io/quote/v3

$ go list -m rsc.io/q...
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0
$

Go 模块的每个不同的主要版本(v1v2 等)都使用不同的模块路径:从 v2 开始,路径必须以主要版本结尾。在示例中,rsc.io/quotev3 不再是 rsc.io/quote:而是,它由模块路径 rsc.io/quote/v3 标识。这种约定称为 语义导入版本控制,它为不兼容的包(具有不同主要版本)提供不同的名称。相反,rsc.io/quotev1.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.2rsc.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 allgo.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 buildgo 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 模块是 Go 中依赖项管理的未来。 模块功能现在可以在所有受支持的 Go 版本(即 Go 1.11 和 Go 1.12)中使用。

本文介绍了使用 Go 模块的这些工作流程

  • go mod init 创建一个新模块,初始化描述它的 go.mod 文件。
  • go buildgo test 及其他包构建命令会根据需要将新的依赖项添加到 go.mod 中。
  • go list -m all 打印当前模块的依赖项。
  • go get 更改依赖项所需的版本(或添加新的依赖项)。
  • go mod tidy 删除未使用的依赖项。

我们鼓励您开始在本地开发中使用模块,并将 go.modgo.sum 文件添加到您的项目中。 要提供反馈并帮助塑造 Go 中依赖项管理的未来,请向我们发送 错误报告经验报告

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

下一篇文章:在 Go 1.12 中调试您部署的内容
上一篇文章:新的 Go 开发者网络
博客索引