11 Go Module 缓存机制详解

前言

初识 Go Module 时,我会有疑问:Go 把 module@version 缓存在本地时,到底缓存了哪些东西、放在哪里、什么时候会命中复用、什么时候必须重新走下载?


首先看一下 GOPATH 下的目录结构,缓存通常都在这(依赖/缓存相关的):

text 复制代码
$GOPATH/
  bin/
  gocache/
  pkg/
    mod/
      cache/
        download/
    sumdb/
  src/
  • $GOPATH/bingo 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/sumdbsum.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 里常见的 000a 这类两位十六进制目录,是典型的"按哈希前缀分桶",目的只是避免单目录文件过多导致性能劣化。对应实现集中在:

  • 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.goreadModGraph
    • 通过 goModSummary(m) 获取模块的 go.mod 摘要
  • cmd/go/internal/modload/modfile.gorawGoModData
    • 本地/replace 模块(无版本) :直接从目录读 go.mod
    • 远程模块(带版本) :调用 modfetch.GoMod(context.TODO(), m.Path, m.Version)

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 的模块内容缓存是两条并行且互补的链路。

相关推荐
代码游侠2 小时前
学习笔记——Linux内核与嵌入式开发3
开发语言·arm开发·c++·学习
怎么没有名字注册了啊2 小时前
C++ 进制转换
开发语言·c++
代码游侠2 小时前
C语言核心概念复习(二)
c语言·开发语言·数据结构·笔记·学习·算法
冰暮流星2 小时前
javascript之双重循环
开发语言·前端·javascript
墨月白2 小时前
[QT]QProcess的相关使用
android·开发语言·qt
小小码农Come on2 小时前
QT信号槽机制原理
开发语言·qt
KoiHeng2 小时前
Java的文件知识与IO操作
java·开发语言
-Try hard-2 小时前
完全二叉树、非完全二叉树、哈希表的创建与遍历
开发语言·算法·vim·散列表
霍理迪3 小时前
JS作用域与预解析
开发语言·前端·javascript