Go 博客

迁移到 Go 模块

Jean de Klerk
2019 年 8 月 21 日

介绍

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

注意:有关文档,请参见 管理依赖项开发和发布模块

Go 项目使用各种各样的依赖管理策略。 供应商目录 工具,如 depglide 很受欢迎,但它们的行为存在很大差异,并且并不总是能很好地协同工作。一些项目将整个 GOPATH 目录存储在一个 Git 仓库中。其他项目仅仅依赖于 go get 并期望在 GOPATH 中安装相当新的依赖项版本。

Go 的模块系统是在 Go 1.11 中引入的,它提供了一个内置于 go 命令中的官方依赖管理解决方案。本文介绍了将项目转换为模块的工具和技术。

请注意:如果您的项目已经在 v2.0.0 或更高版本上进行了标记,则在添加 go.mod 文件时需要更新您的模块路径。我们将在未来的一篇文章中解释如何在不破坏用户的情况下进行操作,该文章将重点介绍 v2 及更高版本。

将项目迁移到 Go 模块

项目在开始向 Go 模块过渡时可能处于三种状态之一

  • 一个全新的 Go 项目。
  • 一个已建立的 Go 项目,使用非模块依赖管理工具。
  • 一个已建立的 Go 项目,没有任何依赖管理工具。

第一种情况在 使用 Go 模块 中有所介绍;我们将在这篇文章中讨论后两种情况。

使用依赖管理工具

要转换已使用依赖管理工具的项目,请运行以下命令

$ 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.jsonGopkg.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 会找到模块中所有包(包括传递导入的包)所导入的所有包。它会为模块未提供的包添加新的模块要求,并会删除对未提供任何导入包的模块的要求。如果一个模块提供了仅被未迁移到模块的项目导入的包,那么该模块要求将用 // 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 get 会根据需要降级其他依赖项以保持兼容性。)例如,

$ go mod why -m rsc.io/binaryregexp
[...]
$ go mod graph | grep rsc.io/binaryregexp
[...]
$ go get rsc.io/[email protected]
$

没有依赖管理工具

对于没有依赖管理系统的 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 将创建一个仅包含 modulego 指令的 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 tidygo buildgo test 中看到错误。如果发生这种情况,请尝试使用 go get 降级到旧版本(例如,go get github.com/broken/[email protected]),或者花时间使您的模块与每个依赖项的最新版本兼容。

模块模式下的测试

迁移到 Go 模块后,一些测试可能需要调整。

如果测试需要在包目录中写入文件,那么当包目录位于模块缓存(只读)中时,它可能会失败。特别是,这可能会导致 go test all 失败。测试应该将需要写入的文件复制到临时目录中。

如果测试依赖于相对路径(../package-in-another-module)来定位和读取另一个包中的文件,那么如果该包在另一个模块中,它将失败,该模块将位于模块缓存的版本化子目录或 replace 指令中指定的路径中。如果是这种情况,您可能需要将测试输入复制到您的模块中,或者将测试输入从原始文件转换为嵌入在 .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/lintgithub.com/golang/lint 都解析到包含托管在 go.googlesource.com/lint 上的代码的仓库。该仓库中包含的 go.mod 文件 声明其路径为 golang.org/x/lint,因此只有该路径对应于有效的模块。

Go 1.4 提供了一种使用 // import 注释 声明规范导入路径的机制,但包作者并不总是提供它们。因此,在模块之前编写的代码可能使用了模块的非规范导入路径,而没有对不匹配产生错误。在使用模块时,导入路径必须与规范模块路径匹配,因此您可能需要更新 import 语句:例如,您可能需要将 import "github.com/golang/lint" 更改为 import "golang.org/x/lint"

在某些情况下,模块的规范路径可能与它的仓库路径不同,比如 Go 模块 2.0 版本或更高版本。Go 模块的大版本号大于 1 时,它的模块路径必须包含大版本号后缀:例如,版本 `v2.0.0` 必须包含后缀 `/v2`。但是,`import` 语句可能在引用模块内的包时**不包含**这个后缀。例如,`github.com/russross/blackfriday/v2` 在 `v2.0.1` 版本中,非模块用户可能使用 `github.com/russross/blackfriday` 导入它,因此需要更新导入路径,添加 `/v2` 后缀。

结论

对于大多数用户来说,转换到 Go 模块应该是一个简单的过程。偶尔可能会遇到非规范导入路径或依赖项中的破坏性更改导致的问题。未来文章将探讨 发布新版本、v2 及更高版本,以及调试奇怪情况的方法。

如果您想提供反馈并帮助塑造 Go 中依赖管理的未来,请向我们发送 错误报告使用体验报告

感谢您一直以来的反馈和帮助,让我们共同改进模块。

下一篇文章: 模块镜像和校验和数据库发布
上一篇文章: 2019 年贡献者峰会
博客索引