Golang依赖包管理知多少(2)-迁移工程到modules模式

前言

在golang工程中可以使用多种依赖管理策略,像dep、glide这种Vendoring工具非常的流行,但是它们又各自为政并不能很好的兼容。很多项目把工程文件放在GOPATH目录下存储为一个Git仓库,其他人通过go get命令依赖存放在GOPATH中最新版本的代码。

Go modules系统在Go1.11时发布,它通过go命令提供了一个官方的依赖管理解决方案。这篇文章主要讲述了迁移原有工程到modules的一些工具和技术。

注意:如果你的工程已经标记为v2.0.0或者更高版本,在增加go.mod文件时需要更新工程中的module path。我们将会在后面的文章中解释如何做才能在v2或者更高版本时不让你的用户糟心。

原工程中使用了依赖管理工具

为了转换一个使用依赖管理工具的工程,需要执行以下命令:

$ 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的参数是module path,通过它来定位一个module的位置。

这是一个暂停执行go build ./..go test ./..的好时机,如果你喜欢迭代方式的话,一步步地修改go.mod文件,go.mod文件就会变成接近modules依赖规范的文件了。

$ 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命令会解决你的module中所有的传递依赖,比如为了必须的packages增加一个新的module、移除那些没有任何packages的module导入。如果一个module只是提供packages导入还没有迁移到modules那这个module依赖就会被标记上// indirect注释,通常在提交go.mod文件到版本管理之前运行一下go mod tidy是一个很好的习惯。

让我们继续执行构建、运行测试用例:

$ go build ./...
$ go test ./...
[...]
$

注意:别的依赖管理工具可能有对独立的packages或者完整的仓库有特殊依赖或者是不能被识别没有放在go.mod中的依赖项。所以你可能获取不到和之前一模一样的packages版本,而且还有升级过去重大变更的风险。因此跟踪审核上面命令的依赖项结果是很重要的。为此执行:

$ 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.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文件,在这个例子中,我们设置module path为golang.org/x/blog,也就是它的导入路径。当前其他用户想要使用这个package的时候就要用到它,所以对它的更改一定要小心。module指令描述了module path,go指令描述了在module内部编译时希望使用的golang版本。

下一步运行go mod tidy命令来增加这个module的依赖:

$ 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命令会添加所有module传递依赖的packages到你的module中同时生成一个go.sum文件存储每个library版本和HASH值,让我们构建、测试一下确保OK:

$ 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命令时,它会自动使用依赖module的最新版本。如果在你的GOPATH目录中包含一个老版本的依赖项时会中断变更,子啊go buildgo testgo mod tidy命令执行时会显示错误信息。出现这种情况下,尝试使用go get来降级到一个老版本或者花点时间把你的module修改一下兼容最新版本的依赖项。

在modules模式下的测试用例

当迁移到Go modules之后许多测试用例可能修改。如果一个测试用例需要写在package目录下并当这个package目录在module缓存中时,它就会执行失败,因为此时它是只读的。在特定情况下这就可能导致go test all执行失败。这需要将这些文件拷贝一个临时目录中去。???

如果测试用例存放在一个相对路径下(../package-in-another-module),在另外的package中可读,它也会执行失败,因为它会被定位到当前module缓存版本管理的子目录中去。如果是这种情况,你需要将这些测试的输入放到你的module中或者将这些测试输入原始文件转换为.go源代码文件。???

如果一个测试用例希望使用go命令在GOPATH模式下运行测试,也会失败。这种方式需要添加一个go.mod文件到你的源代码树中来执行测试,或者设置GO111MODULE=off变量。

发布一个版本

最后你打一个tag,发布你的module新版本,如果你还没有做好发布准备,这也是可选的。如果你还没发布一个正式版本,下游用户就会使用了特殊commitId作为伪版本号,不建议使用这样的方式。

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

一个新的go.mod文件描述了你的module的导入路径,增加了一些最小版本依赖,如果你的用户已经使用了你的导入路径并且你的module中依赖项没有重大更改那么增加go.mod文件是向后兼容的。但如果有重大变更那可能带来一些问题。如果你之前已经存在tag标签,你应该把你的次版本号加1。详细的Go modules发布可以参考如何发布版本、增量一个版本

导入和规范module path

每一个module在它的go.mod文件中都声明一个module path。每一条import语句都必须写明module path作为前缀,但是go命令会遇到同一代码仓库使用不同的远程导入路径。比如, golang.org/x/lintgithub.com/golang/lint都指向托管在go.googlesource.com/lint的仓库。go.mod文件中声明了此代码库中path是golang.org/x/lint,那么对于这个module来说只有这一个路径是有效的。

在Go1.4中提供一个方法,通过// import comments注释来声明一个规范的path,但是包提供者通常没有使用这种方式。结果就是之前写代码的时候就使用不规范的path导入而不会出现错误,当使用了module时这些导入path必须启用到规范的module path时就需要修改import语句,比如:你可以需要修改github.com/golang/lintgolang.org/x/lint

另外一种场景是一个module的规范path和代码仓库路径不相同,比如主版本为2或者更高的module。一个Go module主版本号如果为2及以上,必须在module path中包含一个主版本后缀,比如版本v2.0.0必须包含v2后缀。然而import语句已经引用了没有后缀的packages。比如一个非module用户以github.com/russross/blackfriday使用的是github.com/russross/blackfriday/v2的v2.0.1 package,那么他就需要更新一下import path包含v2后缀。

综述

迁移原工程到Go modules是一个简单的过程,偶尔的问题可能是不规范的导入路径或者是依赖项的重大变更导致的。