翻译自 Go 官方博文 Migrating To Go Modules。
Jean de Klerk 21 August 2019
这篇文章是系列文章的第二部分。
Go 项目使用多种依赖管理策略,其中对 vendor 包的管理有两个比较流行的工具 dep 和 glide,但他们在行为上有很大的差异,而且并不是总能很好地同时使用。一些项目将其整个 GOPATH 目录存储在一个 Git 仓库中。其他人则只依赖于 go get
并期望在GOPATH中安装较新版本的依赖项。
Go 的模块系统在 Go 1.11 中引入,它提供了一个内置在 Go 命令中的官方依赖管理解决方案。本文描述了将项目转换为模块的相关工具和技术。
请注意:如果您的项目已经标记为 v2.0.0 或更高版本,则在添加 go.mod 文件时需要更新模块路径。我们将在以后的一篇文章中解释如何做到这一点,而不会破坏您的用户。
在开始过渡到 Go 模块时,项目可能处于下面三种状态之一:
第一种情况是在使用 Go Modules中介绍的;我们将在本文中讨论后两种情况。
若要转换已使用依赖管理工具的项目,请运行以下命令:
$ git clone https://github.com/my/project
[...]
$ cd project
$ cat Godeps/Godeps.json
{
"ImportPath": "github.com/my/project",
"GoVersion": "go1.12",
"GodepVersion": "v80",
"Deps": [
{
"ImportPath": "rsc.io/binaryregexp",
"Comment": "v0.2.0-1-g545cabd",
"Rev": "545cabda89ca36b48b8e681a30d9d769a30b3074"
},
{
"ImportPath": "rsc.io/binaryregexp/syntax",
"Comment": "v0.2.0-1-g545cabd",
"Rev": "545cabda89ca36b48b8e681a30d9d769a30b3074"
}
]
}
$ go mod init github.com/my/project
go: creating new go.mod: module github.com/my/project
go: copying requirements from Godeps/Godeps.json
$ cat go.mod
module github.com/my/project
go 1.12
require rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca
$
go mod init
创建一个新的 go.mod 文件,并自动从 Godeps.json、Gopkg.lock 或许多其他受支持的格式导入依赖项。go mod init
参数是模块路径,表示模块的位置。
现在是暂停并运行 go build
和 go test
的好时机。后面的步骤可能会修改你的 go.mod 文件,所以如果您喜欢采用迭代的方法,当前的 go.mod 文件最接近于之前的依赖说明。
$ go mod tidy
go: downloading rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca
go: extracting rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca
$ cat go.sum
rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca h1:FKXXXJ6G2bFoVe7hX3kEX6Izxw5ZKRH57DFBJmHCbkU=
rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
$
go mod tidy
找到所有依赖的包及间接依赖的包,添加到 go.mod 文件中,并删除无用的依赖包。如果直接依赖包未启用 Go Module,那么间接依赖包的尾部将被注释 // indirect
标记。在将 go.mod 文件提交到版本库之前,最好先运行 go.mod tidy
。
让我们确保代码可以成功构建和测试:
$ go build ./...
$ go test ./...
[...]
$
请注意,其他依赖管理器可能在单个包或整个代码仓库(而不是模块)级别指定依赖项,并且通常不识别依赖项的 go.mod 文件中指定的依赖项。因此,您可能不会得到与以前完全相同的每个包的版本,并且有升级变更带来的风险。因此,遵循上述命令并对结果的依赖项进行审计是很重要的。要做到这一点,请运行:
$ go list -m all
go: finding rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca
github.com/my/project
rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca
$
并将结果版本与旧的依赖管理文件进行比较,以确保所选版本是适当的。如果您发现的版本不是您想要的,您可以使用 go mod why -m
和/或 go mod graph
找出原因,然后使用 go get
并升级或降级到正确的版本。例如:
$ go mod why -m rsc.io/binaryregexp
[...]
$ go mod graph | grep rsc.io/binaryregexp
[...]
$ go get rsc.io/binaryregexp@v0.2.0
$
对于没有依赖关系管理系统的 Go 项目,首先创建一个 go.mod 文件:
$ git clone https://go.googlesource.com/blog
[...]
$ cd blog
$ go mod init golang.org/x/blog
go: creating new go.mod: module golang.org/x/blog
$ cat go.mod
module golang.org/x/blog
go 1.12
$
如果没有来自以前的依赖项管理器的配置文件,go mod init
将创建一个只包含 module 和 go 指令的 go.mod
文件。在本例中,我们将模块路径设置为 golang.org/x/blog,因为这是它的自定义导入路径。用户可以使用此路径导入包,我们必须小心不要随意更改它。
module 指令声明模块路径,go 指令声明用于编译模块内代码 Go 语言的预期版本。
接下来,运行 go mod tidy
以添加模块的依赖项:
$ go mod tidy
go: finding golang.org/x/website latest
go: finding gopkg.in/tomb.v2 latest
go: finding golang.org/x/net latest
go: finding golang.org/x/tools latest
go: downloading github.com/gorilla/context v1.1.1
go: downloading golang.org/x/tools v0.0.0-20190813214729-9dba7caff850
go: downloading golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7
go: extracting github.com/gorilla/context v1.1.1
go: extracting golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7
go: downloading gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637
go: extracting gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637
go: extracting golang.org/x/tools v0.0.0-20190813214729-9dba7caff850
go: downloading golang.org/x/website v0.0.0-20190809153340-86a7442ada7c
go: extracting golang.org/x/website v0.0.0-20190809153340-86a7442ada7c
$ cat go.mod
module golang.org/x/blog
go 1.12
require (
github.com/gorilla/context v1.1.1
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7
golang.org/x/text v0.3.2
golang.org/x/tools v0.0.0-20190813214729-9dba7caff850
golang.org/x/website v0.0.0-20190809153340-86a7442ada7c
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637
)
$ cat go.sum
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
git.apache.org/thrift.git v0.0.0-20181218151757-9b75e4fe745a/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
[...]
$
go mod tidy
添加了模块依赖的所有包和间接依赖包,并生成一个 go.sum 文件,其中包含每一个依赖包特定版本的校验和。让我们在结束时确保代码仍能生成并通过测试:
$ go build ./...
$ go test ./...
ok golang.org/x/blog 0.335s
? golang.org/x/blog/content/appengine [no test files]
ok golang.org/x/blog/content/cover 0.040s
? golang.org/x/blog/content/h2push/server [no test files]
? golang.org/x/blog/content/survey2016 [no test files]
? golang.org/x/blog/content/survey2017 [no test files]
? golang.org/x/blog/support/racy [no test files]
$
请注意,当 go mod tidy
添加一个依赖时,它会添加该模块的最新版本。如果您的 GOPATH 包含了一个较旧版本的依赖项,随后发布了一个突破性的更改,您可能会看到 go mod tidy
、go build
或 go test
发生错误。如果发生这种情况,请尝试使用 go get
(例如,go get github.com/broken/module@v1.1.0) 降级到旧版本,或者花点时间使您的模块与每个依赖项的最新版本兼容。
有些测试在迁移到 Go 模块后可能需要调整。
如果测试需要在包目录中写入文件,则当包目录位于模块缓存中时,它可能会失败,因为模块缓存是只读的。特别是,这可能导致 go test all
失败。测试时应该将需要写入的文件复制到临时目录中。
如果测试依赖于相对路径(…/package-in-another-module)来定位和读取另一个包中的文件,且依赖的包位于另一个模块中,测试将失败。该模块将位于模块缓存的版本子目录或 replace 指令中指定的路径。如果是这样的话,您可能需要将测试输入拷贝到您的模块中,或者将测试输入从原始文件转换为嵌入在 .go 源文件中的数据。
如果期望测试中的 Go 命令以 GOPATH 模式运行,则可能会失败。如果是这样的话,您可能需要向被测试的源码文件目录中添加一个 go.mod 文件,或者显式地设置 GO111MODULE=OFF。
go env -w GO111MODULE=OFF
最后,您应该标记并发布模块的新版本。如果还没有发布任何版本,这是可选的。但是如果没有正式版本,下游用户将依赖于使用伪版本的特定提交,这可能更难支持。
$ git tag v1.2.0
$ git push origin v1.2.0
新的 go.mod 文件为您的模块定义了一个规范的导入路径,并添加了 Go 最低版本要求。如果您的用户已经使用了正确的导入路径,并且您的模块还没有进行中断性的更改,那么添加 go.mod 文件是向后兼容的,但这是一个重大的更改,可能会暴露出现有已知的问题。如果有现有的版本标记,则应该增加 minor version。请参阅 Publishing Go Modules,以了解如何增加和发布版本。
每个模块在 go.mod 文件中声明其模块路径。每个引用模块中的包的导入语句都必须将模块路径作为包路径的前缀。但是,有时可能会遇到通过不同的导入路径提供包的代码仓库,比如 golang.org/x/lint 和 github.com/golang/lint 都指向模块 go.googlesource.com/lint。代码库中包含的 go.mod 文件声明其路径为 golang.org/x/lint,因此只有该路径对应于一个有效模块。
Go 1.4 提供了一种使用 // import comments
规范导入路径的机制,但是包作者并不一定会提供它们。因此,在模块之前编写的代码可能对模块使用了非规范的导入路径,但并不会出现不匹配的错误。在使用模块时,导入路径必须与模块规范路径匹配,因此可能需要更新 import 语句:例如,您可能需要将import “github.com/golang/lint”
更改为 import “golang.org/x/lint”
。
对于主要版本 2 或更高版本的 Go 模块,会出现另一种情况,即模块的规范路径可能与其仓库路径不同。一个主要版本高于 1 的 Go 模块必须在其模块路径中包含一个主版本后缀:例如,版本 v2.0.0 必须具有后缀 /v2。但是,import 语句可能引用了模块中没有该后缀的包。例如,非模块用户使用 github.com/russrose/blackfriday/v2 包对应于版本 v2.0.1 可能已经将其导入为github.com/russrose/blackfriday,那么需要更新导入路径以包含 /v2 后缀。
对于大多数用户来说,转换到 Go 模块应该是一个简单的过程。由于非规范的导入路径或依赖项中的破坏更改,可能偶尔会出现的问题。以后的文章将探讨发布新版本、v2 及更高版本以及调试异常情况的方法。
如果想提供反馈并帮助塑造 Go 依赖管理的未来,请发送错误报告或经验报告。
感谢您的反馈和帮助以改进 Go 模块。