
前言
初识 Go Module 时,我会有疑问:Go 把 module@version 缓存在本地时,到底缓存了哪些东西、放在哪里、什么时候会命中复用、什么时候必须重新走下载?
首先看一下 GOPATH 下的目录结构,缓存通常都在这(依赖/缓存相关的):
text
$GOPATH/
bin/
gocache/
pkg/
mod/
cache/
download/
sumdb/
src/
$GOPATH/bin:go install产物(可执行文件)默认输出目录;如果设置了GOBIN,会优先输出到GOBIN。$GOPATH/gocache:构建缓存目录(build cache),缓存编译/链接的中间产物与结果,用于加速重复的go build/test/install;实际位置以环境变量GOCACHE为准。$GOPATH/src:早期 GOPATH 模式下的源码工作区(import path会映射到这里找包)。进入 Go Module 时代后,项目源码通常不需要放在这里,但它仍可能存在于你的机器上。$GOPATH/pkg:缓存相关的落盘区域。在 Go Module 语义下,这里最核心的是:$GOPATH/pkg/mod(= 默认GOMODCACHE):模块缓存(解压后的源码树 + 对应版本的下载物)。$GOPATH/pkg/mod/cache/download:下载物缓存(.mod/.info/.zip等按module@version复用的版本文件)。$GOPATH/pkg/sumdb:sum.golang.org(或你配置的GOSUMDB)相关的校验数据缓存,用于加速/复用模块校验与透明日志查询。
1. 先把几个目录分清:Module Cache / Download Cache / Build Cache
在 Go Module 语义下,"缓存"至少分成三类(职责完全不同):
1.1 GOMODCACHE:模块缓存(module cache)
默认位置:GOMODCACHE 未显式设置时,通常落在 $GOPATH/pkg/mod
- 可用源码树(解压目录) :
<module>@<version>/...
1.2 GOMODCACHE/cache/download:下载物缓存(download cache)
这个目录里放的是"从 GOPROXY(或直连)下载回来的版本文件":
@v/<version>.mod@v/<version>.info@v/<version>.zip
只要版本不变,下次不需要再去网络下载。
1.3 GOCACHE:构建缓存(build cache)
它缓存的是编译/链接过程的中间产物(按 action ID 等键命中),与 "pkg/mod 里有没有某个模块版本" 并不等价。
GOCACHE 里常见的 00、0a 这类两位十六进制目录,是典型的"按哈希前缀分桶",目的只是避免单目录文件过多导致性能劣化。对应实现集中在:
src/cmd/go/internal/cache/
2. GOMODCACHE 里到底缓存了什么
同一个 module@version 通常会同时落两份东西:
- 下载物缓存 :
.mod/.info/.zip(版本文件) - 解压后的源码目录 :
<module>@<version>/...(本地可读的目录树)
比如第一次用到 golang.org/x/text@v0.14.0 时,Go 会同时缓存:cache/download/.../@v/v0.14.0.{mod,info,zip} 这些版本文件,以及解压后的 golang.org/x/text@v0.14.0/ 源码目录树。
2.1 下载物(版本文件):复用的基本单位
在 GOMODCACHE/cache/download 下,按 "模块路径(转义后)/@v/" 存放:.mod/.info/.zip。
同一版本下次再用,优先直接命中这里,不必重复走网络。
2.2 解压目录(源码树):编译/加载包需要
.zip 只是压缩包;真正给 go list/build 用的是解压后的目录树:GOMODCACHE/<module>@<version>/...。
一旦 import path 归属到某个 module@version,后续就是在这棵目录树里定位包目录并读源码。
3. 从源码切入"按需下载":modload 触发,modfetch 获取并写入缓存
modload:在构建依赖图、解析 import 归属时,发现"缺少某个模块版本的内容",就触发获取modfetch:执行获取与落盘:先查缓存;没命中才走网络;拿到就写入缓存
当读取 go.mod 构建依赖图时会触发。
Go 1.25.0 的调用链(modload 侧):
cmd/go/internal/modload/buildlist.go:readModGraph- 通过
goModSummary(m)获取模块的go.mod摘要
- 通过
cmd/go/internal/modload/modfile.go:rawGoModData- 本地/replace 模块(无版本) :直接从目录读
go.mod - 远程模块(带版本) :调用
modfetch.GoMod(context.TODO(), m.Path, m.Version)
- 本地/replace 模块(无版本) :直接从目录读
modfetch 侧的处理(实现集中在 cmd/go/internal/modfetch/cache.go):
- 先尝试读本地缓存(例如
readDiskGoMod一类逻辑) - 不命中时按
GOPROXY逐个尝试(TryProxies),通过Lookup(...).GoMod(...)获取,然后写入缓存
构图阶段通常只需要 go.mod ;拉 .zip、解压出源码目录,会推迟到需要读源码树(比如编译/加载包)时再触发。
4. 缓存复用:版本文件缓存 + 解压目录缓存 + 校验链路
在相同的 module@version ,Go 的默认行为是尽可能复用本地缓存,避免重复下载。
4.1 缓存规则
Go Module 的基本单位是 module@version,而不是 "仓库某个分支的最新状态"。只要版本相同,就有稳定的缓存路径与稳定的版本文件名。
例如:项目依赖 github.com/sirupsen/logrus@v1.9.3。
- 第一次使用:会把
v1.9.3的版本文件写入$GOMODCACHE/cache/download/github.com/sirupsen/logrus/@v/,并在需要源码时解压到$GOMODCACHE/github.com/sirupsen/logrus@v1.9.3/。 - 之后只要还是
v1.9.3:就按这些固定路径直接命中缓存,不用再下载。
4.2 modfetch 缓存写入
不论来源是 proxy 还是 direct,modfetch 的核心策略都是:
- 能读缓存就读缓存
- 需要网络时才发请求
- 拿到结果就写入 "下载物缓存" 与(必要时)"解压目录"
这使得下一次同样的请求很自然地从磁盘返回,而不是再走一遍网络链路。
源码入口集中在:
src/cmd/go/internal/modfetch/src/cmd/go/internal/modfetch/codehost/
4.3 缓存校验
缓存查到了,但是万一被篡改了,Go 有校验机制。
- 如果
go.sum已有对应条目,会用它验证下载/缓存内容 - 是否需要 checksum database(sumdb)参与,受
GOSUMDB/GONOSUMDB/GOPRIVATE等影响
校验失败会直接破坏"可复用性",缓存不会被当作可信输入继续使用。
5. pkg/mod 里的 !stack!exchange 是什么:为大小写不敏感文件系统做的路径转义
你在 Windows 上观察 GOMODCACHE 时,经常会看到路径里出现 !,例如:
.../pkg/mod/!stack!exchange/...
它不是随机命名,也不是某种"特殊模块"。它是 Go 对 module path 的一条固定转义规则:
- 为了避免大小写不敏感文件系统上的路径冲突,把大写字母编码成
!+ 小写
这条规则来自 golang.org/x/mod/module(例如 EscapePath / EscapeVersion 一类 API),cmd/go 在把模块路径映射到磁盘目录名时会统一走这套转义。
modload 在需要某类模块内容时触发,modfetch 尽可能以 "读缓存 → 不命中才网络 → 成功就写缓存" 的方式把 module@version 落盘;而 GOCACHE 只是构建中间产物的另一个缓存维度,和 pkg/mod 的模块内容缓存是两条并行且互补的链路。