:::info
日期:2019 年 08 月 21 日
作者:Jean de Klerk
原文链接:https://go.dev/blog/migrating-to-go-modules
:::
介绍
这篇文章是系列文章的第 2 部分。
- 第 1 部分 - 使用 Go 模块
- 第 2 部分 - 迁移到 Go 模块(本文)
- 第 3 部分 - Go 发布模块
- 第 4 部分 - Go 模块:v2 及更高版本
- 第 5 部分 - 保持模块兼容
Go 项目使用了各种各样的依赖管理策略。 诸如 dep 和 glide 之类的供应商工具很受欢迎,但它们在行为上存在很大差异,并且并不总是能很好地协同工作。 一些项目将其整个 GOPATH 目录存储在单个 Git 存储库中。 其他人只是依赖 go get 并期望在 GOPATH 中安装相当新版本的依赖项。
Go 1.11 中引入的 Go 模块系统提供了内置于 go 命令中的官方依赖管理解决方案。 本文介绍了将项目转换为模块的工具和技术。
请注意:如果您的项目已标记为 v2.0.0 或更高版本,则在添加 go.mod 文件时需要更新模块路径。 我们将在以后专注于 v2 及更高版本的文章中解释如何在不破坏用户的情况下做到这一点。
将你的项目迁移到 Go 模块
在开始向 Go 模块过渡时,项目可能处于以下三种状态之一:
- 一个新的品牌项目。
- 具有非模块依赖项管理器的已建立的 Go 项目。
- 一个没有任何依赖管理器的已建立的 Go 项目。
第一种情况在 Using 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 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 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 将创建一个只有模块和 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) 来定位和读取另一个包中的文件,如果该包位于另一个模块中,它将失败,该模块将位于模块的版本化子目录中 缓存或替换指令中指定的路径。 如果是这种情况,您可能需要将测试输入复制到您的模块中,或者将测试输入从原始文件转换为嵌入在 .go 源文件中的数据。
如果测试期望测试中的 go 命令在 GOPATH 模式下运行,则它可能会失败。 如果是这种情况,您可能需要将 go.mod 文件添加到要测试的源树中,或者明确设置 GO111MODULE=off。
发布版本
最后,您应该为新模块标记并发布发布版本。 如果你还没有发布任何版本,这是可选的,但如果没有正式发布,下游用户将依赖于使用伪版本的特定提交,这可能更难以支持。
$ git tag v1.2.0
$ git push origin v1.2.0
您的新 go.mod 文件为您的模块定义了一个规范的导入路径,并添加了新的最低版本要求。 如果您的用户已经在使用正确的导入路径,并且您的依赖项没有进行重大更改,那么添加 go.mod 文件是向后兼容的——但这是一个重大更改,并且可能会暴露现有问题。 如果您有现有的版本标签,您应该增加次要版本。请参阅发布 Go 模块以了解如何增加和发布版本。
导入和规范模块路径
每个模块在其 go.mod 文件中声明其模块路径。每个引用模块内包的 import 语句都必须将模块路径作为包路径的前缀。但是,go 命令可能会通过许多不同的远程导入路径遇到包含该模块的存储库。例如,golang.org/x/lint 和 github.com/golang/lint 都解析为包含托管在 go.googlesource.com/lint 上的代码的存储库。该存储库中包含的 go.mod 文件将其路径声明为 golang.org/x/lint,因此只有该路径对应于有效模块。
Go 1.4 提供了一种使用 // 导入注释声明规范导入路径的机制,但包作者并不总是提供它们。因此,在模块之前编写的代码可能对模块使用了非规范的导入路径,而不会出现不匹配的错误。使用模块时,导入路径必须与规范模块路径匹配,因此您可能需要更新导入语句:例如,您可能需要将 import “github.com/golang/lint” 更改为 import “golang.org/x/皮棉”。
模块的规范路径可能与其存储库路径不同的另一种情况发生在主要版本 2 或更高版本的 Go 模块中。主版本高于 1 的 Go 模块必须在其模块路径中包含主版本后缀:例如,版本 v2.0.0 必须具有后缀 /v2。但是,import 语句可能引用了模块中没有该后缀的包。例如,github.com/russross/blackfriday/v2 v2.0.1 的非模块用户可能已将其导入为 github.com/russross/blackfriday,并且需要更新导入路径以包含 /v2 后缀。
总结
对于大多数用户来说,转换为 Go 模块应该是一个简单的过程。 由于非规范的导入路径或依赖项中的破坏性更改,可能会偶尔出现问题。 未来的帖子将探讨发布新版本、v2 及更高版本,以及调试奇怪情况的方法。
为了提供反馈并帮助塑造 Go 依赖管理的未来,请向我们发送错误报告或体验报告。
感谢您的所有反馈并帮助改进模块。