概述

最近我将一些项目的依赖管理工具从 dep 迁移到了 go module,有一些爽的地方,也有一些不爽的地方,所以这里就简单介绍一下我的迁移历程中一些个人的观点感受。

先提一下,截止到我写这篇文章的时候,Go 的最新 release 版本是 1.16。

dep

好处

  • 指定版本很友好

    Dep 中支持在 Gopkg.toml 指定一个 package 的特定版本,而且支持多种语义,例如:

    • version:指定使用特定的版本,可以是 >,=,< ... ...
    • branch:指定使用特定的分支
    • revision:指定使用特定的 commit
  • 支持保存代码中没有用到的 package

    Dep 可以通过 ignored 选项来过滤代码中没有使用到的依赖,这对于引用一些工具是非常有用的。例如我们创建 mock 的工具,可以直接放在 vendor 里面,然后 ci 的时候通过 go install vendor/xxx 来安装;在 go module 中,则不行了,常见的用法是创建一个 tools.go 的文件,然后里面 import _ xxxx

痛点

  • 连接管理不稳定

    这是最多同学吐槽的点,就是 dep 经常一个 dep ensure 命令就是一天,然后还完不成,这其实就是 dep 连接管理和重试的问题了;此外,还有问题就是 dep 的本地缓存也处理得不是很好,经常会出现远程 master 分支已经更新了,但是本地缓存因为处于另外一个分支(例如是 v5 这个 release 分支),但是 dep 就是反应不过来,不会切换到 master 分支之类,然后就导致你 dep ensure 的时候它报错说找到你想要的那个版本。

    这个时候,我常用的处理方式就是 rm -rf $GOPATH/pkg/dep/sources/git---xxxx 清除掉本地的缓存,然后再 dep ensure 一遍就可以了。

  • 无法更新指定 package

    例如我想只更新一个 github.com/pkg/errors 的版本,其他无关的依赖不更新,那么对不起,不支持这个操作,我只能使用 dep ensure 更新所有的 package。

    如果我就是这么执拗,就只想更新这个 package,可以,我需要把所有的项目直接使用到的其他 package 的当前版本在 Gokpkg.toml 里面都指定好版本了,这样就不会被更新到了。

  • 包管理奇怪

    • 例如我想添加一个依赖,如果我在项目中已经 import github.com/pkg/errors 了,那么这个时候我用 dep ensure -add github.com/pkg/errors 会提示我已经使用了这个 package,如果我想将这个依赖更新到 Gopkg.lock 里面,那么我只能 dep ensure

go module

好处

  • 多版本支持

    如果你的项目想同时使用一个 Module 的多个版本,例如主要的代码都是使用一个最新版本的 Module,但是可能因为兼容旧版本的接口之类的,你还需要在某些 API 调用的地方使用比较旧版本的 Module,那么 go module 可以帮助你做到,因为它支持在 import module 的时候指定版本。

  • 支持环境变量设置

    比较常用的:

    • GOPRIVATE:私有项目,不使用 Proxy
    • GOPROXY:支持 proxy,在内网 CI 时格外有用
  • 简单直接的包依赖管理

    例如我想添加一个新的依赖包,直接使用 go get xxx 即可被添加在 go.mod 中;想升级,直接就 go get -u 即可;如果想指定版本,也是直接 go get xxx@commit-id

  • 支持撤消发布

    这是一个新的特性,如果你使用了一个错误发布的版本,那么 go mod 会提醒你,然后你就需要进行升级或者降级了。

缺点

  • 包版本管理不友好

    虽然前面说了 go module 的包依赖管理很简单,但是,对于一些 tag 来说就不友好了,例如我想安装一个 v5 版本的 tag:go get github.com/godbus/dbus@v5.0.3,对不起,你会遇到这个问题:

    1. [root@liqiang.io]# go get github.com/godbus/dbus@v5.0.3
    2. go get: github.com/godbus/dbus@v5.0.3: invalid version: module contains a go.mod file, so major version must be compatible: should be v0 or v1, not v5

    那么,我需要怎么做呢?下面两种方式都是官方允许的,其中,推荐第一种(虽然我觉得很别扭,而且升级到 v6 之后还得修改代码,但是人家也是认为这是 feature,见优点 1):

    1. [root@liqiang.io]# go get github.com/godbus/dbus/v5
    1. [root@liqiang.io]# go get github.com/godbus/dbus@37bf87eef99d69c4f1d3528bd66e3a87dc201472
  • go 生态的版本管理不好

    go mod 认为所有被管理的包都是遵循语义化版本(见我的文档中有是说明),但是,实际上有些包是不讲武德的,例如最近我被坑的 grpc-go 这个包,x.y.z 版本的不同 y 版本之间居然是不兼容的,这都属于 google 自己家的东西你让我情何以堪(不是说 Google 没有 KPI 压力的吗,还这么分裂的么)。

    除了 grpc-go,另外一个常用的被人吐槽的库就是 etcd 了,但是因为我没怎么用(很久以前用了,但是那时还没有 go mod,而且可能那时还是比较简单的,没有太多版本的问题)。

  • 其他命令会检查 go.mod 版本

    我遇到过的常见命令就是go rungo test,他们都会检查一遍 go.sum 中的版本是否和 go.mod 以及项目中的依赖匹配,如果不匹配时,他们就会更新一把。

    有时这让我很心累,例如前面提到的我要向后兼容限制 grpc-go 的版本,经常就被升上去了(还是那个只能限制最小版本的锅啊)。

对比感受

  • 大家都支持替换 package 的源地址
    • dep:override
    • gomodule:replace
  • 大家都有创建和填充 vendor 目录的功能
    • dep ensure --vendor-only
    • go mod vendor
  • 都不能很好地解决私有项目的问题
    • 内网不受信任的 github
    • github 上的私有项目
    • 这两个场景都需要设置本地的 git config 来解决

整体来看,go module 还是值得升级了,唯一让我留念 dep 的 feature 就是精准的 version 指定,在 go module 中,没法做到精准的指定,即使你指定了,也是认为这是最低要求版本 “>=”,后面很可能因为有依赖的其他 package,会将这个 package 的版本升上去,当然,偶尔也是可以满足你的指定,就是使用你想要的那个版本的。

让我最舒心的就是我提交代码不用加 vendor 目录,而是使用一个 private 的 PROXY Server,这样 CI 速度不受影响,代码提交记录也很清爽,是个非常不过的改进。

Ref