包包baobaolin.com
date
entry
050
topic
tooling
rev
—

go-build 快取清完不到三天又是 8.1G:Go 只刪五天沒用的項目,go run 的每個新版本執行檔也存進去

10 月 18 日凌晨 1 點,這台 Mac 的 ~/Library/Caches/go-build 是 8.1G。68.5 小時前它才被 go clean -cache 清過,再往前十天又清過一次。Go 的建置快取會自己清理,但規則是刪掉五天沒用的項目,不管總共多大;在一台每天都在建置的機器上,這幾天的量就足夠吃掉一大塊磁碟。

Go 的建置快取目錄裡有一份 README,第二行寫著:「Run "go clean -cache" if the directory is getting too large.」go help cache 則說 go 指令會定期刪掉最近沒用到的資料,也說快取會正確反映原始碼、編譯器和選項的變化,一般使用不需要手動清。兩段話合起來,聽起來像是「它會自己顧好,真的太大再清」。

但我手上的 du -sh 紀錄(當時終端機裡的輸出,沒有另外存檔)是:10 月 5 日 9.2G,清掉;10 月 15 日凌晨 4 點半 3.6G,又清掉;同一天中午 12 點 100M,到 10 月 18 日凌晨 1 點又是 8.1G,第三次清掉。這篇要談的是這個快取為什麼長得比清得快,依序是:自動清理的實際規則、在獨立目錄裡重現清理、哪些建置讓它長得快、四種 go clean 各刪什麼,最後是我打算怎麼調整。

自動清理:看的是多舊,不是多大

go help cache 只說「最近沒用到」,而且它說的「不需要手動清」講的是正確性,不是磁碟用量。具體數字在原始碼。Go 1.27.2 的 cmd/go/internal/cache/cache.go 有三個常數:

mtimeInterval = 1 * time.Hour
trimInterval  = 24 * time.Hour
trimLimit     = 5 * 24 * time.Hour

讀到一個快取項目時,如果它的修改時間已經是一小時以前,go 會嘗試把它更新成現在(原始碼註明是 best-effort)。清理不是背景排程:go build、go run、go test 這類執行建置工作的指令,在結束時關閉快取並呼叫 Trim()(go list 不算),它先看快取目錄裡的 trim.txt(上次清理開始的 Unix 秒數),不到 24 小時就跳過。真的清的時候,刪掉修改時間早於「五天加一小時前」的項目。程式註解說五天這個數字來自 Go 開發者一個月的使用統計(golang.org/issue/22990):幾乎所有重複使用都發生在上次使用後五天內。

所以這個機制限制的是「多舊」,程式裡沒有任何容量上限。從 10 月 15 日 4 點半清完,到 18 日 1 點的 8.1G,中間是 68.5 小時;這段時間新產生的項目,沒有一個達到五天的門檻,自動清理不會因為年齡刪掉它們。

在獨立目錄裡看它清

我不想動這台機器實際在用的快取,所以把 GOCACHE 指到暫存目錄,對一個引用 net/http 的小程式做下一節那些建置,累積到 320,156 KiB。接著用 Python 把裡面所有檔案和子目錄的修改時間改成六天前,trim.txt 不動,再 go build 一次:大小沒變,因為上次清理還不到 24 小時。把 trim.txt 改成兩天前的秒數,再 go build:

entries+dirs backdated 6d, trim.txt recent: 320156 KiB
trim.txt 2d ago, after go build: 103264 KiB  mach-o left: 0 0

go run 存的執行檔全部被刪掉了(go run 的執行檔放在以 -d 結尾的子目錄裡,所以子目錄的時間也要一起改,只改檔案的話它們不會被清)。清理後總量是 103,264 KiB,找不到任何 Mach-O 執行檔。剩下的內容我沒有逐項分類;總量的變化符合「最近用到或寫入的項目會留下」的規則。

什麼讓它長得快

同一個暫存快取,從空的開始,每一步之後用 du -sk 量(完整輸出存在實驗目錄的 exp.log):

第一次 go build(含用到的標準函式庫)   104,736 KiB
第一次 go run                           +3,408 KiB
之後每改一行再 go run(五次)            每次 +3,400 KiB(快取裡共 6 個執行檔,20,568,300 位元組)
不改原始碼,再 go run 一次               +0
改完 go build -o,三次                   合計 +144 KiB
GOOS=linux GOARCH=amd64 第一次           +85,736 KiB
go build -race 第一次                    +109,132 KiB

go run 那一行是 Go 1.24 之後才有的。Go 1.24 的版本說明寫:「Executables created by go run and the new behavior of go tool are now cached in the Go build cache.」後面接著說這是「at the expense of making the cache larger」。go build -o 把執行檔寫到你指定的地方,快取裡只多了編譯的中間產物;go run 則把連結出來的執行檔留在快取裡。快取的鍵是建置輸入的雜湊,所以不改原始碼重跑不會多一份;改了程式、連結出快取裡還沒有的執行檔,才會多一份(改回之前的版本,如果那份還在,就直接重用)。

這個小程式的執行檔只有 3.3 MiB。真的專案大得多:10 月 18 日 1 點清完之後大約十分鐘,我再量一次,快取已經是 719M(du -sh),裡面有兩個 Mach-O 執行檔,各 54.2 MB,檔案時間相差約 17 秒,大小只差 304 位元組。這台機器上當時有幾個 go run 在跑,符合「改一下、再跑一次」的樣子,但哪一個行程寫了哪一個檔,我沒有追。

用假設的數字算一下量級:如果一個專案的執行檔是 54 MB,一天改完重跑 30 次、每次都連結出快取裡還沒有的執行檔,就是一天約 1.6 GB;五天內的全部留著,就是約 8 GB。這只是示範機制的算式,不是這台機器的實際組成;8.1G 那次清掉之前,我沒有分類量過裡面是什麼。第一次交叉編譯成 linux/amd64 給容器用、第一次用 -race 建置,也會為那次用到、快取裡還沒有的標準函式庫和相依套件各多出一組編譯產物。

四種 go clean 各刪什麼

go help clean 列了四個和快取有關的旗標,刪的東西不一樣:

-cache       建置快取裡的 00–ff 子目錄與 log.txt(不含 fuzz/)
-testcache   寫一個時間戳讓測試結果過期,不刪建置結果
-modcache    整個模組下載快取(GOMODCACHE,含解開的原始碼)
-fuzzcache   模糊測試存下的輸入(快取裡的 fuzz/)

-cache 的實作在 cmd/go/internal/clean/clean.go:用 glob 找 [0-9a-f][0-9a-f] 的子目錄刪掉,再刪 log.txt,不刪快取目錄本身;cache.go 的註解也寫明 fuzz/ 不歸 -cache 和自動清理管。我在暫存快取上跑完 go clean -cache,目錄裡剩 README 和 trim.txt,佔 8 KiB。代價是之後要重新編譯,快取的測試結果也沒了,下次 go test 會真的重跑;它不碰模組和原始碼,是四個裡面我最放心用的。-testcache 只是寫一個時間戳,不會立刻空出空間。

-fuzzcache 要想一下再刪。文件說這些是「expand code coverage」的輸入,刪掉之後模糊測試會暫時變得比較沒效率,直到重新找到同樣覆蓋率的輸入;它們不是 testdata 裡的語料,go clean 不會動 testdata。

-modcache 刪的是下載下來的模組。之後的建置如果需要它們,就得重新下載;那時沒有網路或代理伺服器出問題(而且專案沒有 vendor),就建置不了。這台機器的 ~/go/pkg/mod 是 4.7G,其中 cache/download 佔 843M。另外,這個目錄用 rm -rf 清不乾淨。我在暫存目錄下載一個模組(github.com/google/uuid@v1.6.0)試:

dr-xr-xr-x  … modcache2/github.com/google/uuid@v1.6.0
rm rc=1
files left:       31
rm: …/uuid@v1.6.0/go.mod: Permission denied
go clean -modcache rc=0

解開的模組目錄預設是唯讀的(除非建置時加 -modcacherw),rm 刪不掉唯讀目錄裡的檔案。go clean -modcache 會先把目錄改成可寫再刪。

我打算怎麼調整

回到開頭那三次清除:每次都是磁碟快滿時才動手。10 月 15 日那次清完不到三天,快取又是 8.1G。如果是我重新安排,我會先決定這個快取最多可以佔多少,再看要不要減少產生的量,而不是等磁碟滿。

具體來說,常常改完就跑的專案,我會改成 go build -o 到專案裡被 git 忽略的目錄再執行,執行檔就不會在快取裡一份一份累積(編譯的中間產物還是會進快取)。交叉編譯和 -race 這種偶爾才跑的,可以給它們獨立的 GOCACHE,用完整個刪掉;代價是和日常建置共用的部分要重新編譯一次。需要立刻空出空間時,用 go clean -cache;-modcache 和 -fuzzcache 則要先想清楚接下來會不會需要。

如果只記得一件事

Go 的建置快取只刪五天沒用的項目,沒有容量上限;從 1.24 起,go run 連結出來的每個不同的執行檔也算一個項目。每天都在建置的機器上,它的大小取決於最近新增了多少、還在持續使用的舊項目有多少,以及清理實際什麼時候跑;不只是最近五天產生的東西。要控制大小,從產生的量下手,或者自己排清理;go clean -cache 不碰模組和原始碼,模組快取用 go clean -modcache,不要用 rm -rf。

修訂紀錄

  1. 首次發布