使用 Go Module
引言
Go 1.11 和 1.12 包含对 Module(后文称模块) 的初步支持, Go 的新依赖管理系统使依赖版本信息明确且易于管理. 本文介绍了开始使用模块所需的基本操作. 后续文章将涵盖发布供其他人使用的模块.
模块是存储在文件树中的 Go 包的集合, 其根目录中包含 go.mod 文件. go.mod 文件定义了模块的模块路径, 它也是用于根目录的导入路径, 以及它的依赖(它们是成功构建所需的其他模块). 每个依赖都被写作模块路径和特定语义版本.
从 Go 1.11 开始, 如果目录在 $GOPATH/src 之外, 则 go 命令允许在当前目录或任何具有 go.mod 的父目录使用模块. (在 $GOPATH/src 中, 为了兼容性, go 命令仍然在旧的GOPATH 模式下运行, 即使找到了 go.mod 也是如此. 有关详细信息, 请参阅 go 命令文档) 从 Go 1.13 开始, 模块模式将是所有开发的默认模式.
本文将介绍使用模块开发 Go 代码时出现的一系列常见操作:
- 创建一个新的模块.
- 添加依赖.
- 升级依赖.
- 添加一个新的大版本依赖.
- 升级依赖到一个新的大版本.
- 移除不用的依赖.
创建一个新的模块
让我们创建一个新模块.
在 $GOPATH/src之 外的某处创建一个新的空目录, 切换到该目录, 然后创建一个新的源文件 hello.go:
package hello |
我们也在 hello_test.go 中写一个测试:
package hello |
此时, 该目录包含一个包, 但不包含模块, 因为没有 go.mod 文件. 如果我们切换到 /home/gopher/hello 并立即运行 go test, 我们会看到:
$ go test |
最后一行总结了整个的包测试. 因为我们在 $GOPATH 之外以及任何模块之外工作, 所以 go 命令不知道当前目录的导入路径, 并根据目录名称构造了一个假路径: _/home/gopher/hello.
让我们使用 go mod init 将当前目录作为模块的根目录, 然后再次尝试 go test:
$ go mod init example.com/hello |
恭喜! 您已经编写并测试了第一个模块.
go mod init 命令写入了一个 go.mod 文件:
$ cat go.mod |
go.mod 文件仅出现在模块的根目录中. 子目录中的包所有的导入路径, 包含了模块路径和子目录的路径. 例如, 如果我们创建了一个子目录 world, 我们就不需要(也不想)在它那运行 go mod init. 该包将自动被识别为 example.com/hello 模块的一部分, 其导入路径为 example.com/hello/world.
添加一个依赖
Go 模块的主要动机是改善使用(即添加依赖)其他开发人员编写的代码的体验.
让我们更新我们的 hello.go 来导入 rsc.io/quote 并使用它来实现 Hello:
package hello |
现在让我们再次运行测试:
$ go test |
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 |
第二个 go 测试命令不会重复这项工作, 因为 go.mod 现在是最新的, 下载的模块在本地缓存(在 $GOPATH/pkg/mod中):
$ go test |
请注意, 虽然 go 命令可以快速轻松地添加新的依赖项, 但它并非没有成本. 现在, 你的模块依赖于临界区的新依赖关系, 例如正确性, 安全性和适当的许可, 仅举几例. 有关更多注意事项, 请参阅 Russ Cox 的博客文章”我们的软件依赖性问题“.
如上所述, 添加一个直接依赖通常也会带来其他间接依赖. go list -m all 命令列出当前模块及其所有依赖项:
$ go list -m all |
在 go list 输出中, 当前模块(也称为主模块)始终是第一行, 后面是按模块路径排序的依赖项.
golang.org/x/text 版本 v0.0.0-20170915032832-14c0d48ead0c 是伪版本的示例, 它是特定无标记提交的 go 命令的版本语法.
除了 go.mod 之外, go 命令还维护一个名为 go.sum 的文件, 其中包含特定模块版本内容的预期加密哈希:
$ cat go.sum |
go 命令使用 go.sum 文件来确保这些模块的未来下载与第一次下载相同, 以保证你项目所依赖的模块不会出现意外更改, 无论是出于恶意, 偶然还是其他原因. go.mod 和 go.sum 都应检入版本控制.
升级依赖
使用 Go 模块, 版本使用语义版本标记引用. 语义版本包含三个部分: major, minor 和 patch. 例如, 对于 v0.1.2, 主要版本为 0, 次要版本为 1, 补丁版本为 2. 让我们来看看几个小版本的升级. 在下一节中, 我们将考虑进行主要版本升级.
从 go list -m all 的输出中, 我们可以看到我们正在使用 golang.org/x/text 的无标记版本. 让我们升级到最新的标记版本并测试是否一切仍然奏效:
$ go get golang.org/x/text |
哇噢! 都通过了. 让我们再看一下 go list -m all 和 go.mod 文件:
$ go list -m all |
golang.org/x/text 包已升级到最新的标记版本(v0.3.0). go.mod 文件也已更新到指定的 v0.3.0. indirect 注释表示此模块不直接使用依赖关系, 而是仅由其他模块依赖关系间接使用. 有关详细信息, 请参阅 go help modules.
现在让我们尝试升级 rsc.io/sampler 次要版本. 通过运行 go get 和 running tests 以相同的方式启动:
$ go get rsc.io/sampler |
呃, 哦! 测试失败表明最新版本的 rsc.io/sampler 与我们的用法不兼容. 让我们列出该模块的可用标记版本:
$ go list -m -versions rsc.io/sampler |
我们一直在使用 v1.3.0; v1.99.99 显然不好. 也许我们可以尝试使用 v1.3.1 代替:
$ go get rsc.io/sampler@v1.3.1 |
请注意 go get 参数中的显式 @v1.3.1. 通常, 传递给 go get 的每个参数都可以采用显式版本; 默认值为 @latest, 它解析为之前定义的最新版本.
添加一个新的大版本依赖
让我们为我们的包添加一个新函数: func Proverb 通过调用 quote.Concurrency 返回一个 Go 并发谚语, 该函数由模块 rsc.io/quote/v3 提供. 首先我们更新 hello.go 以添加新功能:
package hello |
然后我们为 hello_test.go 添加一个测试:
func TestProverb(t *testing.T) { |
然后我们可以测试我们的代码:
$ go test |
请注意, 我们的模块现在依赖 rsc.io/quote 和 rsc.io/quote/v3:
$ go list -m rsc.io/q... |
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 |
我们可以更新 hello.go 中 quote.Hello(), 以使用 quoteV3.HelloV3():
package hello |
然后我们不再需要重命名的导入, 所以可以撤消:
package hello |
让我们重新运行测试以确保一切正常:
$ go test |
移除未使用的依赖
我们已经删除了对 rsc.io/quote 的使用, 但它仍然在 go list -m all 和 go.mod 文件中:
$ go list -m all |
为什么? 因为构建一个单独的包, 比如 go build 或 go test, 可以很容易地判断什么时候缺少并且需要添加, 但是不知道什么时候可以安全地删除. 只有在检查模块中的所有包以及这些包的所有可能的构建标记组合之后, 才能删除依赖. 普通的构建命令不会加载此信息, 因此无法安全地删除依赖.
go mod tidy 命令清除这些未使用的依赖:
$ go mod tidy |
结论
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 中依赖管理的未来, 请向我们发送错误报告或体验报告.
感谢您的所有反馈和帮助改进模块.
作者: Tyler Bui-Palsulich, Eno Compton