golang 1.13 - 依赖管理从 dep 到 mod 踩坑

接触 golang 很晚,实际用来开发大概在 1.9 左右,所以我的主要印象是在 1.9 、 1.10 上的,依赖管理经过一些尝试之后,选择了 『官方』(后来实际被抛弃了)的 dep(《golang 依赖管理:glide 从入门到放弃》)。

后来 1.11、1.12 推出了 module (亦即 go mod 命令),考虑到尚不稳定又有切换成本,就继续留守在 vendor 目录上。

2019 年 9月终于 1.13 出来了,做了几个比较大的改动,同时 module 也终于转正,所以我终于下定决心迁移到 1.13,并改用 mod 做依赖管理。

概要

迁移过程主要参考了《干货满满的 Go Modules 和 goproxy.cn》(实操主要是 #快速迁移项目至 Go Modules 部分),讲得非常清楚,也推荐大家参考,一些细节就不再赘述,只强调我踩坑的地方。

简单来说是这么个过程:

  • 卸载原有的 go,并下载安装 1.13 版本。

    官网 golang.org 因为是谷歌的服务器,也在屏蔽之列,部分同学可能连访问这个都有困难,其实国内有官方的镜像站 golang.google.cn 。

  • go env -w 重新设置你的环境变量 。

    注意 go env 的内容保存在 $HOME/.config/go/env ,不会覆盖原来的系统环境变量。在读取环境变量时, go env 的值优先。为了避免后续增加判断环境变量的负担,建议 go env 里有的、只有 go 会读取的环境变量,在系统环境变量里删除。

    因为国情特殊,一定要设置 GOPROXY。

  • 针对依赖工具和项目情况迁移(以下主要讲这部分的坑)。因为我之前用的是 dep,下面全部是关于从 dep 的迁移。

特别提一下一个细节,在安装好的 1.13 下获取 mod 子命令的帮助,下面有一段提醒:

1
2
3
4
5
6
7
>go help mod
Go mod provides access to operations on modules.
Note that support for modules is built into all the go commands,
not just 'go mod'. For example, day-to-day adding, removing, upgrading,
and downgrading of dependencies should be done using 'go get'.
See 'go help modules' for an overview of module functionality.

大意渣翻:

注意对 modules 的支持已经内建在所有 go 子命令内,而不仅仅是 ‘go mod’ 。

举例说,每天添加、移除、升级、降级依赖,都应该使用 ‘go get’ 完成。

也就是说所有跟依赖管理相关的命令,譬如 go get ,都是用新逻辑在处理。

踩坑

坑01:GOPROXY 特定情况不起效

本来直接在项目根目录敲 go mod init <mod_path> ,是可以自动从 Gopkg.tomlGopkg.lock 导入依赖信息,自动完成迁移的。但是在国内直接这样做是会出错的,部分包获取超时(留意 golang.org/x/ 开头的包):

1
2
3
4
5
6
7
8
9
10
>go mod init myapp
go: creating new go.mod: module myapp
go: copying requirements from Gopkg.lock
go: converting Gopkg.lock: stat github.com/360EntSecGroup-Skylar/excelize@v2.0.0: github.com/360EntSecGroup-Skylar/excelize@v2.0.0: invalid version: module contains a go.mod file, so major version must be compatible: should be v0 or v1, not v2
go: converting Gopkg.lock: stat golang.org/x/text@v0.3.0: unrecognized import path "golang.org/x/text" (https fetch: Get https://golang.org/x/text?go-get=1: dial tcp 216.239.37.1:443: connectex: A connection attempt failed because the connected party did not properly respond after a period of time,
or established connection failed because connected host has failed to respond.)
go: converting Gopkg.lock: stat gopkg.in/russross/blackfriday.v2@d3b5b032dc8e8927d31a5071b56e14c89f045135: gopkg.in/russross/blackfriday.v2@v2.0.1: invalid version: go.mod has non-....v2 module path "github.com/russross/blackfriday/v2" at revision v2.0.1
go: converting Gopkg.lock: stat golang.org/x/net@9b4f9f5ad5197c79fd623a3638e70d8b26cef344: unrecognized import path "golang.org/x/net" (https fetch: Get https://golang.org/x/net?go-get=1: dial tcp 216.239.37.1:443: connectex: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.)
go: converting Gopkg.lock: stat golang.org/x/image@61b8692d9a5c9886248d7f96e0ba50ad77baab4c: unrecog
nized import path "golang.org/x/image" (https fetch: Get https://golang.org/x/image?go-get=1: dial tcp 216.239.37.1:443: connectex: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.)

这其实是一个 bug,init 导入依赖部分没有引用 GOPROXY。已经有人向官方提交了 issue,只是修复不知道要等到哪个版本合入。当前可以创建一个空白的 go.mod 然后执行 go mod tidy 来绕过。

go.mod 只需要包含 module path 和 go 版本即可:

1
2
3
module myapp
go 1.13

坑02:import 和 module path 不一致

增加了 go.mod 之后,执行 go mod tidy

1
2
3
4
5
6
7
>go mod tidy
go: finding github.com/mojocn/base64Captcha latest
......
go: myapp/model/data imports
github.com/go-xorm/core: github.com/go-xorm/core@v0.6.3: parsing go.mod:
module declares its path as: xorm.io/core
but was required as: github.com/go-xorm/core

这个好解决,把项目里对这个 module 的引用,都指向它声明的路径即可。

下面梳理一下出错的原因:

xorm.io/core 这个 module,同时在 github.com 和 xorm.io 都有提供访问。

在使用 dep 的时候,从哪个路径 import,都是可行的。当我从 github.com/go-xorm/core import 时,dep 就从 github 下载,并保存到 vendor/github.com/go-xorm/core 下。到编译的时候,到 vendor 下对应的路径去找。也就是无论选哪个,下载、保存、import 的路径,三者是对应的就行

但是 mod 会读取 module 的 go.mod,它自称是 xorm.io/core ,那么从 github import 就是非法的。也就是现在要 加上跟 module 自身 go.mod 声明的路径一致。(当然,现在保存不在 vendor 目录了,而是在 $HOME/go/pkg/mod/ 底下,从原来每个项目存一份,变成每个 module 的每个版本,全局只存一份。)

退一步讲,如果将来 xorm.io 因为某些原因不再提供访问,而 github 那份还在,可以在 go.mod 通过 replace 关键字将下载地址指向 github,但其余的路径,依然要跟声明的路径保持一致(主要是 import 路径,下载保存是自动的,并不需要人工干预)。

坑03:最新版本 module 不兼容

好了,依赖的分析和获取终于不报错了,go.modgo.sum 也成功生成了。接下来让我们编译一下:

1
2
3
4
5
6
7
8
>make build
go build -ldflags '-s -w'
go: finding gopkg.in/yaml.v2 v2.2.2
// 省略若干行...
# myapp/model/data
model\data\csv.go:59:6: xng.QuoteStr undefined (type *xorm.Engine has no field or method QuoteStr)
model\data\csv.go:61:6: xng.QuoteStr undefined (type *xorm.Engine has no field or method QuoteStr)
make: *** [Makefile:28: build] Error 2

Engine.QuoteStr() 是一个返回当前数据库引擎使用的引号的方法,我当时特意使用这个方法,用来同时兼容不同的数据库,避免额外的判断。所以我确定这个方法是存在的。

这很容易想到是版本兼容性的问题。查看 go.mod 里的是 v0.7.9,再翻看 Gopkg.toml 之前用的是 v.0.7.1 。坑爹的是,新版本居然删掉了这个方法。

这里稍微提一下 版本号的问题 。go mod 强制使用 SemVer(如果不知道什么是 SemVer,看这篇 语义化版本 2.0),默认大版本没有改动的话,一定是兼容修改。所以会自动获取当前大版本下最新的版本,并不会参考 Gopkg.toml 的版本。不过话说回来, 即使按照 SemVer 的语义,也没办法埋怨 xorm 的团队,1.0 之前的版本默认为不稳定版本,没有义务保持兼容。

1
>go mod edit -require=github.com/go-xorm/xorm@v0.7.1

这样就可以指定依赖的版本。如果你觉得敲命令太麻烦,直接手动改 go.mod 也可以。一般不推荐直接改,因为你的修改会在下次更新时被覆盖,唯独版本信息是会保留的。

改完再 tidy 一次。

坑04:module path 不统一

再编译一次:

1
2
3
4
5
6
7
8
>make build
go build -ldflags '-s -w'
go: finding github.com/go-xorm/xorm v0.7.1
// 省略若干行...
# myapp/model/data
model\data\engine.go:117:17: cannot use level (type "xorm.io/core".LogLevel) as type "github.com/go-
xorm/core".LogLevel in argument to xng.SetLogLevel
make: *** [Makefile:28: build] Error 2

还是 xorm 的错误。还记得我们在 坑02 中的修改吗,在 go mod 底下,要统一按照声明路径去 import。

因为 坑03 ,xorm 改回了跟我代码兼容的 v0.7.1 ,那与之关联的 core 呢?跟上面类似地,看前后的配置,之前用的 v0.6.0 ,现在的是 v0.7.2 。关键的一点是,在 2019 年 6 月,在这两个版本之间的 v0.6.3,module path 从 github.com/go-xorm/core 改成了 xorm.io/core , xorm 对它的引用在那个时间也做了相应的修改。

为了跟 v0.7.1 的 xorm 兼容,必须使用 < v0.6.3 的 core —— 实际上直接使用 v0.6.0 是最保险的。因为回退到了修改 module path 之前的版本,所以 坑02 的修改白改了,回退掉。

当然记录 坑02 仍然有意义,它提醒我 有时声明的 module path 未必和仓库地址一致

跟上面类似,core 包改回对应的 v0.6.0 ,重新 tidy。

坑05:主版本号变更

再一次编译:

1
2
3
4
5
6
7
>make build
go build -ldflags '-s -w'
# myapp/api
api\survey.go:189:9: assignment mismatch: 2 variables but ex.GetCellValue returns 1 values
// 省略若干相似的错误...
api\survey.go:216:9: too many errors
make: *** [Makefile:28: build] Error 2

依然是版本的兼容问题。

不过这次的错误跟前面的比,是反过来的:我的代码引用的是最新的 v2 代码,这在原来 dep 下是不需要区分包名的。但在 mod 里,大于 1 的大版本是需要体现在路径里的。

在 module 眼里,主版本号不同,相当于两个不同的 module。 这是因为根据 SemVer 的约定,大版本号的改变,意味着引入了 breaking changes。那么如果很不巧地,代码 直接 / 间接 依赖同一个包的不同大版本时,mod 是可以同时导入的,就不会存在依赖上的冲突。

把 import 里 github.com/360EntSecGroup-Skylar/excelize 改为 github.com/360EntSecGroup-Skylar/excelize/v2 ,重新 tidy,这次编译就不再报错了,编译的结果也是可以正常运行的。到此,我踩的坑已经全部记录完毕。

临了 Mark 一篇文章 《再探go modules:使用与细节》。这篇文章对于 go mod 的一些细节做了分析,虽然发表于 1.12 发布前,但是现在来看仍然有效。


知识共享 “署名-非商业性使用-相同方式共享” 4.0 (CC BY-NC-SA 4.0)”许可协议
本文为本人原创,采用知识共享 “署名-非商业性使用-相同方式共享” 4.0 (CC BY-NC-SA 4.0)”许可协议进行许可。
本作品可自由复制、传播及基于本作品进行演绎创作。如有以上需要,请留言告知,在文章开头明显位置加上署名(Jayce Chant)、原链接及许可协议信息,并明确指出修改(如有),不得用于商业用途。谢谢合作。
详情请点击查看协议具体内容。