迁移到Go modules

这是 Go Modules 系列的第二篇文章,主要来说明如何从将现有的项目迁移到 Go Modules。

原文地址:https://blog.golang.org/migrating-to-go-modules


简介

这是 Go Modules 系列的第二篇文章:

  • 使用 Go Modules
  • 迁移到 Go Modules
  • 发布 Go Modules
  • Go Modules:V2 及后续版本
  • 保持 Modules 的兼容性

Go 项目使用的依赖管理工具多种多样。像 dep 和 glide 这样的 vendor 工具很受欢迎,但这些工具在使用方式上有很大的差别,所以通常很难一起使用。有些项目将整个 GOPATH 目录存储在一个 Git 仓库下。而有的人仅仅依靠 go get 来安装依赖,并期望在 GOPATH 中安装最新版本的依赖。

在 Go1.11 中,引入了 Go modules,这是一个官方的依赖管理方案,直接通过内建的 go 命令就可以使用。这篇文章介绍一些工具和方法来帮助项目迁移到 Go modules。

请注意:如果你想项目版本标签已经到了 v2.0.0 或者更高,当你添加 go.mod 文件的时候,你需要更新你的模块路径。我们后续的文章将解释在 v2 或者更高版本中如何在不影响用户的情况下做到这一点。

把你的项目迁移到 Go Modules

在迁移到 Go Modules 之前,项目应该属于以下的三种状态之一:

  • 一个全新的 Go 项目
  • 一个已经创建的项目,使用了依赖管理,但不是 Go Modules
  • 一个已经创建的项目,没有任何依赖管理

第一种情况直接使用上篇文章中所说的方法来使用 Go Modules。下面来讨论后面的两种情况。

已经有依赖管理时

转化一个已经使用了依赖管理工具的项目时,运行以下的命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
$ 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 文件作为你的预模块依赖规范。

1
2
3
4
5
6
7
$ 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 Modules 的包,在 go.mod 文件中将会被添加上 // indirect 标识。在将代码提交之前,最好运行一次 go mod tidy 命令。

然后确认代码的构建和测试都能通过:

1
2
3
4
$ go build ./...
$ go test ./...
[...]
$

请注意,其他依赖项管理器可能在单个包或整个存储库(而不是模块)级别指定依赖项。而一般不能识别 go.mod 文件中的依赖。因此,可能有些包不会得到与以前完全相同的版本,而且还存在破坏性升级的风险。所以,遵循上面命令并对产生的依赖项进行检查很重要。要做到这一点,需要运行下面的命令:

1
2
3
4
5
$ 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 将降低其他依赖版本,以维护兼容性。)举个例子:

1
2
3
4
5
6
$ go mod why -m rsc.io/binaryregexp
[...]
$ go mod graph | grep rsc.io/binaryregexp
[...]
$ go get rsc.io/binaryregexp@v0.2.0
$

没有依赖管理时

对于一个没有依赖管理的项目,首先需要创建一个 go.mod 文件:

1
2
3
4
5
6
7
8
9
10
$ 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.mod 文件,其中只有 module 和 go 的配置。在这个例子中,根据自定义导入路径,我们将模块的路径设置为 golang.org/x/blog。用户可以通过这个路径来导入包,我们需要注意,不要改变这个路径。

module 关键字定义了模块路径,go 关键字定义了在这个模块中编译代码所需要的 Go 语言版本。

接下来,运行 go mod tidy 添加模块的依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
$ 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 文件,其中包含每个库的特定版本的校验和。最后让我们确认所有的构建和测试都能通过:

1
2
3
4
5
6
7
8
9
10
$ 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 Modules 中的测试

在迁移到 Go Modules 后,有些测试需要做些调整。

如果测试需要写包目录下的文件,当包目录在只读的模块缓存中时,它可能会失败。特别是这可能会导致所有的 go test 失败。测试应该将需要写入的文件复制到一个临时目录中。

如果一个测试依赖相对路径(../package-in-another-module)去其他包中定位和读取文件,如果这个包在其他的模块中,该模块将位于模块缓存的版本化子目录中或在 replace 指令中指定的路径中,测试就会失败。

如果测试期望测试中的 go 命令在 GOPATH 模式下运行,那么它可能会失败。如果发生这种情况,你需要添加一个 go.mod 文件到需要被测试的文件目录下,或者显式设置 GO111MODULE=off。

发布一个版本

最后,你应该给你的新模块打上一个标签,并发布一个新正式版本。这个操作是可选的,但是如果没有一个正式的版本,下游的用户就只能使用提交时产生的 pseudo 版本号,这样后续将很难继续支持。

1
2
$ git tag v1.2.0
$ git push origin v1.2.0

你的 go.mod 文件中定义了明确的导入路径,并且添加了新的最小版本需求。如果你的用户已经使用了正确的导入路径,你的依赖也没有造成破坏性的变更,添加 go.mod 就是向后兼容的。但这是一个重要的改变,可能会暴露已经存在的问题。如果你已经打过标签了,你应该增量的发布次版本。详情查看发布 Go Modules 来学习如何增量发布版本。

导入和规范模块路径

每个模块都在 go.mod 文件中定义了模块路径。每个引用模块内的包的 import语句都必须以模块路径作为包路径的前缀。但是,go 命令可能会通过许多不同的远程导入路径解析到同一个包含该模块的仓库。比如 golang.org/x/lint 和 golang.org/x/lint 都会解析到存储这些代码的仓库 go.googlesource.com/lint。这个仓库中的 go.mod 文件声明了路径为 golang.org/x/lint,因此只有该路径对应于有效的模块。

Go 1.4提供了一种使用 // import 注释声明规范导入路径的机制,但不是所有的包作者都会提供这些。因此,在模块出现之前编写的代码可能使用了模块的非规范导入路径,而不会出现不匹配的错误。当使用模块之后,导入路径必须与规范模块路径匹配,因此你可能需要更新导入语句,例如你可能需要更改 import "github.com/golang/lint" 以导入 "golang.org/x/lint"。

在另一个场景中,主版本为 2 或更高版本的模块,模块的规范路径可能与其存储库路径不同。主版本高于 1 的模块必须在其模块路径中包含主版本后缀。例如,版本 v2.0.0 必须包含后缀 /v2。然而,import语句可能引用了模块中没有那个后缀的包。例如,在 v2.0.1 版本中github.com/russross/blackfriday/v2 的非模块用户可能将其导入为github.com/russross/blackfriday,这就需要加上 /v2后缀来更新导入路径。

结论

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

请向我们发送bug 报告体验报告,帮助我们改善 Go 的依赖管理功能。

感谢你所有的反馈和帮助改进模块的建议。


另外,腾讯云区块链方向在大量招人,包括前端、后端、架构师、产品等诸多岗位,如果感兴趣,请把简历投过来 rayjun0412@gmail.com

译 / Rayjun

© 2020 Rayjun    PowerBy Hexo    京ICP备16051220号-1