谈谈go.sum

众所周知,Go 在作依赖管理时会建立两个文件,go.modgo.sum
相比于 go.mod,关于 go.sum 的资料明显少得多。天然,go.mod 的重要性不言而喻,这个文件几乎提供了依赖版本的所有信息。而 go.sum 看上去就是 go module 构建出来的天书,而不是什么人类可读的数据。git

但实际上,平常开发中咱们仍然不得不跟 go.sum 打交道(一般是解决这个文件带来的合并冲突,抑或试图手工调整里面的内容)。若是不了解 go.sum,只凭经验随便涂改,不必定可以改对。所以,为了更好地掌握 Go 的依赖管理,彻底有必要了解 go.sum 的前因后果。github

鉴于涉及 go.sum 的资料是如此地稀少(即便 Go 官方文档中,对于 go.sum 的描述也是支离破碎的),我花了些时间整理了相关的资料,但愿读者能够从中受益。golang

go.sum 的每一行都是一个条目,大体是这样的格式:算法

<module> <version>/go.mod <hash>

或者sql

<module> <version> <hash>
<module> <version>/go.mod <hash>

其中module是依赖的路径,version是依赖的版本号。hash是以h1:开头的字符串,表示生成checksum的算法是初版的hash算法(sha256)。npm

有些项目实际上并无 go.mod 这个文件,因此 Go 文档里提到这个 /go.mod 的 checksum,用了 "possibly synthesized" (也许是合成的)的说法。估计对于没有 go.mod 的项目,Go 会尝试生成一个可能的 go.mod,并取它的 checksum。api

若是只有对于 go.mod 的 checksum,那么多是由于对应的依赖没有单独下载。好比用 vendor 管理起来的依赖,便只有 go.mod 的 checksum。安全

因为 go 的依赖管理背负着沉重的历史包袱,肯定 version 的规则较为复杂。整个过程就像一个调查问卷,须要回答一个接一个的问题: 分布式

1、项目是否打tag? 工具

若是项目没有打 tag,会生成一个版本号,格式以下:
v0.0.0-commit日期-commitID

好比 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=

引用一个项目的特定分支,好比 develop branch,也会生成相似的版本号:
v当前版本+1-commit日期-commitID

好比 github.com/DATA-DOG/go-sqlmock v1.3.4-0.20191205000432-012d92843b00 h1:Cnt/xQ9MO4BiAjZrVpl0BiqqtTJjXUkWhIqwuOCVtWo=

2、项目有没有用 go module?

若是项目有用到 go module,那么就是正常地用 tag 来做为版本号。

好比 github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08=

若是项目打了 tag,可是没有用到 go module,为了跟用了 go module 的项目相区别,须要加个 +incompatible 的标志。

好比 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=

3、项目用的 go module 版本是否是 v2+?

关于 go module v2+ 的特性,能够参考 Go 的官方文档:https://blog.golang.org/v2-go...。简单而言,就是经过让依赖路径带版本号后缀来区分同一个项目里不一样版本的依赖,相似于 gopkg.in/xxx.v2 的效果。

对于使用了 v2+ go module 的项目,项目路径会有个版本号的后缀。

好比 github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=

之因此 Go 会在依赖管理时引入 go.sum 这样的角色,是为了实现下面的目标:

(1)提供分布式环境下的包管理依赖内容校验

不像其余包管理机制,Go 采用分布式的方式来管理包。这意味着缺少一个可供信赖的中心来校验每一个包的一致性。

在主流的包管理机制中,一般存在一个中央仓库来保证每一个发布的版本的内容不会被篡改。好比在 pypi 里面,即便发布过的版本存在严重的bug,发布者也不能从新发布一个一样版本,只能发布一个新版本。(可是却能够删掉已发布的版本抑或删掉整个项目,参考当年 npm 的 leftpad 事件,因此主流的包管理机制并不是严格意义上的 Append Only。不过这并不影响个人论证)

而 Go 并无一个中央仓库。发布者在 GitHub 上给本身的项目打上 0.1 的 tag 以后,依旧能够删掉这个 tag ,提交不一样的内容后再从新打个 0.1 的 tag。哪怕发布者都是老实人,发布平台也可能做恶。因此只能在每一个项目里存储本身依赖到的全部组件的 checksum,才能保证每一个依赖不会被篡改。

(2)做为 transparent log 来增强安全性

go.sum 还有一个很特别的地方,就是它不只仅记录了当前依赖的checksum,还保留了历史上每次依赖的 checksum。这种作法效法了 transparent log 的概念。transparent log 旨在维护一个 Append Only 的日志记录,提升篡改者的做案成本,同时方便审查哪些记录是篡改进来的。根据 Proposal: Secure the Public Go Module Ecosystem 的说法,go.sum 之因此要用 transparent log 的形式记录历史上的每一个checksum,是为了便于 sum db 的工做。

不得不说的是,go.sum 也带来一些麻烦:

(1)容易产生合并冲突

这恐怕是 go.sum 最为人诟病的地方了。因为许多项目都没有经过打tag的方式来管理发布,每一个commit都至关于新发布一个版本,这致使拉取它们的代码时会偶尔往 go.sum 文件里插入一条新记录。go.sum会记录间接依赖的特性,更是让这种状况雪上加霜。这一类的项目带来的影响可不小 —— 我粗略地统计下 go.sum 里这类记录的行数,大概占了总数的 40%。好比 golang.org/x/sys 在某个项目的 go.sum 里就有多达 37 个不一样的版本。

若是只是莫名其妙的行数多,那最多不过是让人皱皱眉。在多人协做且用到几个常常升版本号的内部公共库的场景下,go.sum 会让人头疼。想象这种状况:

公共库原来有版本甲。
开发者A的分支a依赖了公共库版本乙,开发者B的分支b依赖了公共库版本丙。他们分别给 go.sum 添加记录以下:

common/lib 甲 h1:xxx  
common/lib 乙 h1:yyy
common/lib 甲 h1:xxx  
common/lib 丙 h1:zzz

以后公共库发布了版本丁,包含了版本乙和版本丙的功能。
而后合并分支a和分支b到主干,这时候就会有合并冲突。

如今有两个选择:

  1. 把两个中间版本都归入到 go.sum 进来
  2. 既不选乙,也不选丙,直接采用版本丁

不管采用哪一种方法,都须要手动介入。这无疑带来了没必要要的工做量。

(2) 对于胡乱操做的第三方库,缺少约束能力

go.sum 的本意在于提供防篡改的保障,若是拉第三方库的时候发现其实际内容和记录的校验值不一样,就让构建过程报错退出。然而它能作的也就只限于此。go.sum 的检测功能,给库的使用者带来的负担更甚于库的开发者。在有中央仓库保障的其余包管理器里,人们能够在源头上限制那些捣蛋鬼,不让他们随意变动已经发布出去的版本。可是 go.sum 带来的约束纯粹是道德上的。若是一个库乱改已经发布的版本,会让依赖这个库的项目构建失败。对此库的使用者除了咒骂几句,在 issue 或别的地方痛斥做者,而后更新go.sum文件,彷佛也没别的解决办法。犯错的原本是库的做者,麻烦的倒是库的用户。这种设计可算不上高明。一个可能的解决办法是由官方把知名的库的各个版本镜像起来。虽然知名的库一般不会犯乱改已发布版本的错误,可是若是发生了(或者出于某种不可抗力发生了),至少有个镜像可用。然而这又回到单一中央仓库的路子上去。

(3) 实际状况下,手动编辑go.sum不可避免。好比前面举的,编辑go.sum文件解决合并冲突的状况。我也见过有些项目只在go.sum里保留依赖的最新版本的checksum。若是 go.sum 不是彻底由工具管理的,又怎么能保证它必定是 Append Only 呢?若是 go.sum 不是 Append Only 的,又怎么能把它看成 transparent log 使用呢?

相关文章
相关标签/搜索