go.mod和go.sum

使用go官方的依赖管理工具——go module对go项目进行依赖管理时,会生成两个文件:go.mod和go.sum,本文会聊一聊这两个文件相关的话题。

go.mod

go.mod文件是go module工具的核心,它记录了我们项目中直接引用的依赖包名以及版本,我们执行go mod的各种命令,实际上都是在修改go.mod这个文件。

文件内容

module module_url_xxx

go 1.18

require(
    dep1 v0.0.1
    dep2 v1.0.5
    dep3 v0.0.0-20221118072143-89ae0afbf95f
)

require(
    dep4  v0.1.2 // indirect
)

replace(
    dep2 => dep2 v1.0.4
)

文件的第一行,要指定项目的module的url,如github.com/golang/protobuf

go 1.18表示该项目基于go 1.18版本构建。

require中,每一行是一个该项目的依赖包,格式是:包名:版本

replace中可以强制指定包的版本,这主要用于当有依赖冲突时,手动的指定想要的版本。

如何指定版本

当我们代码中import某个依赖,执行go mod tidy命令后,go.mod会自动的添加该依赖,并使用最新的版本。如果希望使用指定的某个版本, 那么直接在go.mod文件中该依赖项后指定版本即可。

在上面的例子中,我们可以看到dep1的版本是v0.0.1,而dep3的版本是v0.0.0-20221118072143-89ae0afbf95f,二者格式不同的原因是dep1的仓库中, 打了v0.0.1的tag,而dep3中,没有打任何tag,所以就使用了主干(master)分支的最新commit作为最新的版本。

所以我们指定版本时,既可以使用tag,也可以使用分支名。

如何解决依赖冲突

依赖冲突指的是:构建项目时,对于依赖项,有多个指定的版本,这时需要选择哪个版本?

举例:当前项目直接依赖了A包的v0.0.5版本和B包的v0.0.6版本,而B包的v0.0.6版本中,直接引用了A包的v0.0.7版本。构建当前项目时,对于如何选择 A包的版本,就发生了冲突,应该选择v0.0.5版本,还是v0.0.7版本呢?

         dep_A v0.0.5
        /
project 
        \
         dep_B v0.0.6 - dep_A v0.0.7

幸运的是,go module会自动的帮我们选择要使用的版本,对于上面的例子,go module最后会选择A包的v0.0.7版本进行构建。而go module处理版本冲突的原则是: 选择能够支持当前项目构建的最小依赖版本。这句话是什么意思,我们下面详细的解释下。

版本名规范

一般项目的版本名都是这种格式v1.2.3

  1. 第一个数字是主版本号。主版本号一致的版本,向下兼容代码,即v1.2.3必须能够兼容v1.1.2。不一致时,不能向下兼容,比如api发生了变化,那么主版本号一定要变化。
  2. 第二个数字是次版本号。当做了向下兼容的功能更新时,这个数字要变化。
  3. 第三个数字修订版本号。当做了向下兼容的功能修订时,这个数字要变化。
  4. 所以,v1.2.3一定兼容v1.1.2的代码,但是v1.1.2的代码不一定兼容v1.2.3,因为v1.2.3中有v1.1.2中不存在的功能。v2.0.0不一定能兼容v1.9.9的代码,因为可能api发生了变化。

go module的版本选择

根据上面所述的版本名规范,我们再去理解:选择能够支持当前项目构建的最小依赖版本 这句话就比较容易了。我们分析继续上面的例子,A包有两个版本 v0.0.5v0.0.7,而v0.0.7是可以兼容v0.0.5的,但v0.0.5可能不兼容v0.0.7,所以能够支持当前项目构建的版本是v0.0.7。 至于为什么说v0.0.7是最小版本,是因为A包可能还有v0.0.8v0.0.9这种更新的版本,但由于v0.0.7就足够构建当前项目了,所以不必使用更大的版本了。

为什么会有 // indirect

indirect表示该依赖是间接依赖,在go1.16之前,其实并不是所有的间接依赖都会添加到go.mod中,但go1.16之后,所有的间接依赖都会出现在go.mod中, 并且require代码块分成了两块,分别是直接依赖和间接依赖。

replace的作用

上面我们介绍了go module的依赖版本选择方式,那如果当依赖冲突时,我们想使用指定的某个版本时,应该如何做呢?这时可以通过replace来指定,使用也很简单, 直接指定版本即可。

go.sum

go.sum文件也是自动生成的,并且该文件不应该我们手动的进行编辑,它的作用是:保证一致性构建

一致性构建指的是:在不同的环境下,构建项目时,都使用相同的依赖项。

go.sum中其实是记录了依赖项代码的hash值,当构建或拉取依赖时,会进行checksum校验。

构建时校验

这里有个疑问:go.mod中已经指定了依赖包名和版本了,这是不是可以保证一致性构建了?其实还不够,因为即使是相同的包名和版本,它的代码可能会被人篡改, 导致构建时使用的依赖代码不同。当进行构建时,会将本地缓存中的该依赖取出并计算hash值,如果与go.sum中记录的一致,那么可以构建,否则就构建失败。

拉取时校验

go module将依赖项写入go.sum文件前,会对依赖项进行校验,当计算出通过go get命令拉下来的依赖项的hash后,go module会从GOSUMDB配置的数据库中, 拉取该依赖的一个官方的hash,如果二者一致,说明该依赖项没问题,可以写入go.sum。默认的GOSUMDB是sum.golang.org,这里包含几乎所有的第三方库, 但肯定是没有包含公司内部的私有仓库的,因此一般都会将公司的gitlab域名配置在GONOSUMDB这个白名单中,这样就不会对公司的私有仓库进行checksum校验了。