如何调试 Go mod 的各种异常

调试 go mod 问题总会令人失去耐心

Posted by ms2008 on October 31, 2020

Go mod 自从诞生之日就带来了太多太多的争议,当然不能否认它的设计初衷是好的。然而在调试其各种异常时,却浪费了太多开发者的时间。可以毫不客气的说,从来没有一种语言的版本管理,能让人如此崩溃

本文记录了一些我的踩坑经验,希望能给还在挣扎中的 Gopher 一些帮助。

go get

先来看看最近我遇到的一个问题:

$ go mod tidy
go: foo.bar.com/foo/bar@v0.1.0: git remote add origin -- https://foo.bar.com/foo/bar.git in /root/goforge/pkg/mod/cache/vcs/e8ae9003fdd7f44246f94c535282e58436b3568a39734a76af8fac7b716a59b2: exit status 128:
        fatal: remote origin already exists.

光从字面上来看,像是一个 git 的问题。一顿 go mod whygo mod graph 操作后,也没有得到什么有价值的信息。

以我的个人摸索经验来看,whygraph 的输出就是一坨垃圾,只会给开发者带来更多的心智负担。

实际上,调试 go mod 问题最好的工具是 go get ,这样可以只会输出异常模块的依赖树,去掉那些烦人的干扰信息。如果再加上 -x 选项后,更是屡试不爽。继续拿我遇到的这个问题开刀:

$ go get -x foo.bar.com/foo/bar
# get https://foo.bar.com/foo/bar?go-get=1
# get https://foo.bar.com/foo/bar?go-get=1: 200 OK (0.508s)
mkdir -p /root/goforge/pkg/mod/cache/vcs # git3 https://foo.bar.com/foo/bar.git
# lock /root/goforge/pkg/mod/cache/vcs/e8ae9003fdd7f44246f94c535282e58436b3568a39734a76af8fac7b716a59b2.lockmkdir -p /root/goforge/pkg/mod/cache/vcs/e8ae9003fdd7f44246f94c535282e58436b3568a39734a76af8fac7b716a59b2 # git3 https://foo.bar.com/foo/bar.git
cd /root/goforge/pkg/mod/cache/vcs/e8ae9003fdd7f44246f94c535282e58436b3568a39734a76af8fac7b716a59b2; git init --bare
0.011s # cd /root/goforge/pkg/mod/cache/vcs/e8ae9003fdd7f44246f94c535282e58436b3568a39734a76af8fac7b716a59b2; git init --bare
cd /root/goforge/pkg/mod/cache/vcs/e8ae9003fdd7f44246f94c535282e58436b3568a39734a76af8fac7b716a59b2; git remote add origin -- https://foo.bar.com/foo/bar.git
0.006s # cd /root/goforge/pkg/mod/cache/vcs/e8ae9003fdd7f44246f94c535282e58436b3568a39734a76af8fac7b716a59b2; git remote add origin -- https://foo.bar.com/foo/bar.git
go: foo.bar.com/foo/bar@v0.1.0: git remote add origin -- https://foo.bar.com/foo/bar.git in /root/goforge/pkg/mod/cache/vcs/e8ae9003fdd7f44246f94c535282e58436b3568a39734a76af8fac7b716a59b2: exit status 128:
        fatal: remote origin already exists.

瞧,go get -x 会帮你把每个步骤的操作都打出来。显然这个问题确实由 git 引起。大概是 go get 拉取模块时,会先创建一个裸仓库,然后 add origin 。问题就发生在 add origin 这步,git 认为已经有一个 origin 存在了。这是为啥呢?不妨去手动复现下:

$ mkdir git_test
$ cd git_test
$ git init
$ git remote add origin -- https://foo.bar.com/foo/bar.git
fatal: remote origin already exists.

继续看看,现在这个仓库的 origin 是啥:

$ git remote -v
origin

看到这里,突然想起我的 git 配置了 origin 默认指向 HEAD

$ git config -l | grep origin
branch.master.remote=origin
remote.origin.push=HEAD

git 2.20 以上已经在一个空的 origin 上继续 add 了

删除了这个选项后:git config --global --unset remote.origin.push, 终于可以拉取成功了。

replace

试想这么一种场景,假设有个项目 foo 依赖 a 的 v1.0.0 版本,而 foo 依赖的 b 依赖了 a 的 v2.0.0 版本。那么这个时候 foo 的 mod 其实最终会依赖 a 的 v2.0.0 版本。如果你需要 foo 强行依赖 v1.0.0 版本,这个时候就派上了 replace 的上场。直接修改 go.mod 文件,添加:replace a => a v1.0.0 即可。

但是实际情况,往往会更复杂。比如 b 需要 a v2.0.0 的一些新特性的话,简单的 replace 往往不能解决这个问题。如何解决呢?自己去处理。比如,Kuma 自己维护了一个 vendored 文件夹 replace 到本地来处理这种问题;Kubernetes 也有个自己的 staging

mod cache

需要注意的是 go 1.12 版本之前,mod cache 并不是并发安全的,同一个环境并发构建可能会产生竞态。而最新的 go 1.15 还提供了环境变量 GOMODCACHE 来指定 mod cache 的位置,CI 工具可以利用这项加快构建速度。