点击上方“腾讯云TStack”关注我们
获取最in云端资讯和海量技术干货
本文作者 / 阿杜
腾讯云高级工程师
热衷于开源、容器和Kubernetes
目前主要从事镜像仓库、边缘计算
以及云原生架构相关研发工作
1
前 言
In the world of software management there exists a dreaded place called “dependency hell.” The bigger your system grows and the more packages you integrate into your software, the more likely you are to find yourself, one day, in this pit of despair.
依赖管理是一个语言必须要解决的问题,而且随着项目依赖的模块量以及复杂程度不断增加会显得更加重要。Go依赖管理发展历史可以归纳如下:
gopkg.in/yaml.v1
,实际代码地址为:https://github.com/go-yaml/yaml
)通过如上历史,我们可以看出:Go依赖管理的发展历史,其实就是Go去google的历史(google内部没有强烈的版本管理需求),同时也是典型的社区驱动开发的例子
接下来,我将详细探讨Go Modules的两大核心概念:semantic version(语义化版本)和Minimal Version Selection, MVS(最小版本选择)
2
原 理
●semantic version●
Go使用semantic version来标识package的版本。具体来说:
这里,只要模块的主版本号(MAJOR)不变,次版本号(MINOR)以及修订号(PATCH)的变更都不会引起破坏性的变更(breaking change)。这就要求开发人员尽可能按照semantic version发布和管理模块(实际是否遵守以及遵守的程度不能保证,参考Hyrum's Law)
●Minimal Version Selection●
A versioned Go command must decide which module versions to use in each build. I call this list of modules and versions for use in a given build the build list. For stable development, today's build list must also be tomorrow's build list. But then developers must also be allowed to change the build list: to upgrade all modules, to upgrade one module, or to downgrade one module.
The version selection problem therefore is to define the meaning of, and to give algorithms implementing, these four operations on build lists:
这里将一次构建(go build)中所依赖模块及其版本列表称为build list,对于一个稳定发展的项目,build list应该尽可能保持不变,同时也允许开发人员修改build list,比如升级或者降级依赖。而依赖管理因此也可以归纳为如下四个操作:
在Minimal version selection之前,Go的选择算法很简单,且提供了 2 种不同的版本选择算法,但都不正确:
第 1 种算法是 go get
的默认行为:若本地有一个版本,则使用此版本;否则下载使用最新的版本。这种模式将导致使用的版本太老:假设已经安装了B 1.1,并执行 go get
下载,那么go get
不会更新到B 1.2,这样就会导致因为B 1.1太老构建失败或有bug
第 2 种算法是 go get -u
的行为:下载并使用所有模块的最新版本。这种模式可能会因为版本太新而失败:若你运行 go get -u
来下载A依赖模块,会正确地更新到B 1.2。同时也会更新到C 1.3 和E 1.3,但这可能不是 A 想要的,因为这些版本可能未经测试,无法正常工作
这 2 种算法的构建是低保真构建(Low-Fidelity Builds):虽然都想复现模块 A 的作者所使用的构建,但这些构建都因某些不明确的原因而变得有些偏差。在详细介绍最小版本选择算法后,我们将明白为什么最小版本选择算法可以产生高保真的构建:
Minimal version selection assumes that each module declares its own dependency requirements: a list of minimum versions of other modules. Modules are assumed to follow the import compatibility rule—packages in any newer version should work as well as older ones—so a dependency requirement gives only a minimum version, never a maximum version or a list of incompatible later versions.
Then the definitions of the four operations are:
Minimal version selection也即最小版本选择,如果光看上述的引用可能会很迷惑(或者矛盾):明明是选择最新的版本(keep only the newest version),为什么叫最小版本选择?
我对最小版本选择算法中'最小'的理解如下:
这里,我们举例子依次对Go Modules最小版本选择的算法细节进行阐述:
●Algorithm 1: Construct Build List●
There are two useful (and equivalent) ways to define build list construction: as a recursive process and as a graph traversal. The recursive definition of build list construction is as follows. Construct the rough build list for M by starting an empty list, adding M, and then appending the build list for each of M's requirements. Simplify the rough build list to produce the final build list, by keeping only the newest version of any listed module.
简单来说可以通过图遍历以及递归算法(图递归遍历)来构建依赖列表。从根节点出发依次遍历直接依赖B1.2以及C1.2,然后递归遍历。这样根据初始的依赖关系(指定版本:A1->B1.2,A1->C1.2),会按照如下路径选取依赖:
首先构建empty build list,然后从根节点出发递归遍历依赖模块获取rough build list,这样rough build list中可能会存在同一个依赖模块的不同版本(如D1.3与D1.4),通过选取最新版本构建final build list(最终的构建列表一个模块只取一个版本,即这些版本中的最新版),如下:
●Algorithm 2. Upgrade All Modules●
一次性升级所有模块(直接&间接)可能是对构建列表最常见的修改,go get -u
就实现了这样的功能。当执行完这个命令后,所有依赖包都将升级为最新版本,如下(Algorithm 1例子基础上进行升级):
这里面添加了新的依赖模块:E1.3,G1.1,F1.1以及C1.3。新rough build list会将新引入的依赖模块和旧的rough build list模块(黄色部分)进行合并,并从中选取最大版本,最终构建final build list(上图红线标识模块)。
为了得到上述结果,需要添加一些依赖模块到A的需求列表中,因为按照正常的构建流程,依赖包不应该包括D1.4和E1.3,而是D1.3和E1.2。这里面就会涉及Algorithm R. Compute a Minimal Requirement List,该算法核心是创建一个能重复构建依赖模块的最小需求列表,也即只保留必须的依赖模块信息(比如直接依赖以及特殊依赖),并存放于go.mod文件中。比如上述例子中A1的go.mod文件会构建如下:
module A1
go 1.14
require (
B1.2
C1.3
D1.4 // indirect
E1.3 // indirect)
可以看到上述go.mod文件中,没有出现F1.1以及G1.1,这是因为F1.1存在于C1.3的go.mod文件中,而G1.1存在于F1.1的go.mod文件中,因此没有必要将这两个模块填写到A1的go.mod文件中;
而D1.4和E1.3后面添加了indirect标记,这是因为D1.4和E1.3都不会出现在B1.2,C1.3以及它们的依赖模块对应的go.mod文件中,因此必须添加到模块A1的需求列表中(go需要依据这个列表中提供的依赖以及相应版本信息重复构建这个模块,反过来,如果不将D1.4和E1.3添加到go.mod,那么最终模块A1的依赖构建结果就会是D1.3以及E1.2)
另外,这里也可以总结出现indirect标记的两种情况:
●Algorithm 3. Upgrade One Module●
相比一次性升级所有模块,比较好的方式是只升级其中一个模块,并尽量少地修改构建列表。例如,当我们想升级到C1.3时,我们并不想造成不必要的修改,如升级到E1.3以及D1.4。这个时候我们可以选择只升级某个模块,并执行go get命令如下(Algorithm 1例子基础上进行升级):
go get C@1.3
当我们升级某个模块时,会在构建图中将指向这个模块的箭头挪向新版本(A1->C1.2挪向A1->C1.3)并递归引入新的依赖模块。例如在升级C1.2->C1.3时,会新引入F1.1以及G1.1模块(对一个模块的升级或者降级可能会引入其他依赖模块),而新的rough build list(红线)将由旧rough build list(黄色部分)与新模块(C1.3,F1.1以及G1.1)构成,并最终选取其中最大版本合成新的final build list(A1,B1.2,C1.3,D1.4,E1.2,F1.1以及G1.1)
注意最终构建列表中模块D为D1.4而非D1.3,这是因为当升级某个模块时,只会添加箭头,引入新模块;而不会减少箭头,从而删除或者降级某些模块;
比如若从 A 至 C 1.3 的新箭头替换了从 A 至 C 1.2 的旧箭头,升级后的构建列表将会丢掉 D 1.4。也即是这种对 C 的升级将导致 D 的降级(降级为D1.3),这明显是预料之外的,且不是最小修改
一旦我们完成了构建列表的升级,就可运行前面的算法 R 来决定如何升级需求列表(go.mod)。这种情况下,我们最终将以C1.3替换C 1.2,但同时也会添加一个对D1.4的新需求,以避免D的意外降级(因为按照算法1,D1.3才会出现在最终构建列表中)。如下:
module A1
go 1.14
require (
B1.2
C1.3
D1.4 // indirect)
●Algorithm 4. Downgrade One Module●
在升级某个模块后,可能会发现bug并需要降级到某个旧版本。当降级某个模块时,会在构建图中将这个模块的高版本删除,同时回溯删除依赖这些高版本的模块,直到查找到不依赖这些高版本的最新模块版本为止。比如这里我们将D降级到D1.2版本,如下(Algorithm 1例子基础上进行降级):
go get D@1.2
这里将D降级为D1.2,会先删除D1.3以及D1.4模块,然后回溯删除B1.2以及C1.2模块,最终确定到B1.1以及C1.1版本(它们分别是B和C不依赖>=D1.3模块的最新版本了),如下:
在确定直接依赖是B1.1以及C1.1之后,会递归将其依赖模块引入,并添加指定版本D1.2,那么按照算法1可以很容易得出构建列表为:A1,B1.1,D1.2(D1.1 vs D1.2),E1.1以及C1.1
同时,为了保证最小修改,其它不需要降级的模块我们需要尽可能保留,比如:E1.2。这样,final build list就为:A1,B1.1,D1.2(D1.1 vs D1.2),E1.2(E1.1 vs E1.2)以及C1.1
另外,根据算法R,降级操作之后A1模块的需求列表变更如下:
module A1
go 1.14
require (
B1.1
C1.1
D1.2 // indirect
E1.2 // indirect)
这里,如果A1最开始依赖的模块是C1.3而非C1.2,那么C1.3将会在构建图中保留下来(因为C1.3并没有依赖D模块)
3
go mod&sum格式
go.mod以及go.sum一般会成对出现在项目根目录中。其中,go.mod负责记录需求列表(用于构建依赖模块);而go.sum用于记录安全性以及完整性校验,下面依次介绍这两种文件格式:
1.go.mod
module tkestack.io/tke go 1.12 replace ( // wait https://github.com/chartmuseum/storage/pull/34 to be merged github.com/chartmuseum/storage => github.com/choujimmy/storage v0.5.1-0.20191225102245-210f7683d0a6 github.com/deislabs/oras => github.com/deislabs/oras v0.8.0 // wait https://github.com/dexidp/dex/pull/1607 to be merged github.com/dexidp/dex => github.com/choujimmy/dex v0.0.0-20191225100859-b1cb4b898bb7 k8s.io/client-go => k8s.io/client-go v0.17.0 ) require ( github.com/AlekSi/pointer v1.1.0 github.com/Azure/go-autorest v13.3.1+incompatible // indirect github.com/Masterminds/semver v1.4.2 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/aws/aws-sdk-go v1.25.7 github.com/bitly/go-simplejson v0.5.0 github.com/blang/semver v3.5.1+incompatible github.com/casbin/casbin/v2 v2.1.2 github.com/chartmuseum/storage v0.5.0 k8s.io/client-go v12.0.0+incompatible ... )
2.go.sum
<模块> <版本>[/go.mod] <哈希>
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= cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.41.0 h1:NFvqUTDnSNYPX5oReekmB+D+90jrJIcVImxQ3qrBVgM= cloud.google.com/go v0.41.0/go.mod h1:OauMR7DV8fzvZIl2qg6rkaIhD/vmgk4iwEw/h6ercmg= contrib.go.opencensus.io/exporter/ocagent v0.4.12/go.mod h1:450APlNTSR6FrvC3CTRqYosuDstRB9un7SOx2k/9ckA= github.com/AlekSi/pointer v1.1.0 h1:SSDMPcXD9jSl8FPy9cRzoRaMJtm9g9ggGTxecRUbQoI= github.com/AlekSi/pointer v1.1.0/go.mod h1:y7BvfRI3wXPWKXEBhU71nbnIEEZX0QTSB2Bj48UJIZE= github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v31.1.0+incompatible h1:5SzgnfAvUBdBwNTN23WLfZoCt/rGhLvd7QdCAaFXgY4= github.com/Azure/azure-sdk-for-go v31.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v35.0.0+incompatible h1:PkmdmQUmeSdQQ5258f4SyCf2Zcz0w67qztEg37cOR7U= github.com/Azure/azure-sdk-for-go v35.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
go.sum文件可以不存在,当go.sum文件不存在时默认会到远程校验数据库进行校验(通过GOSUMDB设置地址),当然也可以设置为不校验(GONOSUMDB)
4
总 结
5
最佳实践
(v0.0.0)-(提交UTC时间戳)-(commit id前12位)
作为版本标识,例如:github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7
replace k8s.io/client-go => ~/go/src/k8s.io/client-go v0.17.0-dirty-fix
go list -m all
;而列举某个依赖模块的所有版本使用:go list -m -versions xxx
,例如:go list -m -versions k8s.io/client-go
go clean -modcache
清理已下载的依赖文件GOPROXY=https://goproxy.cn,direct
,代表先从代理服务器https://goproxy.cn
下载依赖,如果失败(such as 404)则直接从原地址(such as github)下载6
Refs
没看过瘾?这里还有
阿杜系列大作
听说长得好看的人都点了赞和在看!