衆所周知,Go 在作依賴管理時會建立兩個文件,go.mod
和 go.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到主幹,這時候就會有合併衝突。
如今有兩個選擇:
不管採用哪一種方法,都須要手動介入。這無疑帶來了沒必要要的工做量。
(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 使用呢?