談談go.sum
spacewander 2019/12/28 0:23:41
眾所周知,Go 在做依賴管理時會創建兩個文件,go.mod
和 go.sum
。
相比于 go.mod
,關于 go.sum
的資料明顯少得多。自然,go.mod
的重要性不言而喻,這個文件幾乎提供了依賴版本的全部信息。而 go.sum
看上去就是 go module 構建出來的天書,而不是什么人類可讀的數據。
但實際上,日常開發中我們仍然不得不跟 go.sum
打交道(通常是解決這個文件帶來的合并沖突,抑或試圖手工調整里面的內容)。如果不了解 go.sum
,只憑經驗隨便涂改,不一定能夠改對。因此,為了更好地掌握 Go 的依賴管理,完全有必要了解 go.sum
的來龍去脈。
鑒于涉及 go.sum
的資料是如此地稀少(即使 Go 官方文檔中,對于 go.sum
的描述也是支離破碎的),我花了些時間整理了相關的資料,希望讀者可以從中受益。
go.sum
的每一行都是一個條目,大致是這樣的格式:
<module> <version>/go.mod <hash>
或者
<module> <version> <hash> <module> <version>/go.mod <hash>
其中module是依賴的路徑,version是依賴的版本號。hash是以h1:
開頭的字符串,表示生成checksum的算法是第一版的hash算法(sha256)。
有些項目實際上并沒有 go.mod
這個文件,所以 Go 文檔里提到這個 /go.mod
的 checksum,用了 "possibly synthesized" (也許是合成的)的說法。估計對于沒有 go.mod
的項目,Go 會嘗試生成一個可能的 go.mod
,并取它的 checksum。
如果只有對于 go.mod
的 checksum,那么可能是因為對應的依賴沒有單獨下載。比如用 vendor 管理起來的依賴,便只有 go.mod
的 checksum。
由于 go 的依賴管理背負著沉重的歷史包袱,確定 version 的規則較為復雜。整個過程就像一個調查問卷,需要回答一個接一個的問題:
一、項目是否打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=
。
二、項目有沒有用 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=
三、項目用的 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到主干,這時候就會有合并沖突。
現在有兩個選擇:
- 把兩個中間版本都納入到 go.sum 進來
- 既不選乙,也不選丙,直接采用版本丁
無論采用哪種方法,都需要手動介入。這無疑帶來了不必要的工作量。
(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 使用呢?

關于找一找教程網
本站文章僅代表作者觀點,不代表本站立場,所有文章非營利性免費分享。
本站提供了軟件編程、網站開發技術、服務器運維、人工智能等等IT技術文章,希望廣大程序員努力學習,讓我們用科技改變世界。
[談談go.sum]http://www.yachtsalesaustralia.com/tech/detail-97723.html
- 2022-05-19常見排序算法的golang 實現
- 2022-05-19[LeetCode] 1534. Count Good Triplets
- 2022-05-19django_模型層補充
- 2022-05-19django里的orm操作
- 2022-05-19django(6)
- 2022-05-19django框架7
- 2022-05-18Golang第五章:結構體和對象
- 2022-05-18fjango7
- 2022-05-18golang 的net包的網絡編程 TCP | HTTP | RPC
- 2022-05-18Go基礎3:函數、結構體、方法、接口