Go无内置缓存框架,高效缓存取决于控制缓存键生成、响应拦截、过期判断和头设置四要素;直接包装http.Handler无效因ServeMux不拦截响应,须用自定义ResponseWriter捕获WriteHeader/Write,键含Method+URL+Accept-Encoding,POST/PUT默认不缓存,推荐ristretto本地缓存并严格对齐Cache-Control头。

Go 没有“缓存框架”,只有可组合的缓存组件;真正高效的缓存策略不取决于选哪个库,而在于你是否控制了缓存键生成、响应拦截、过期判断和头设置这四个关键点。
为什么直接包装 http.Handler 大概率不生效
因为标准 http.ServeMux 不会拦截响应体,也不会重写 WriteHeader 和 Write。你写的 handler 只是“路过”请求,没机会捕获响应内容。
- 必须用自定义
ResponseWriter包裹原始http.ResponseWriter,在Write后把bytes.Buffer内容存入缓存 - 缓存键不能只用
r.URL.Path—— 要包含r.Method+r.URL.String()+r.Header.Get("Accept-Encoding"),否则 gzip 和非 gzip 响应会互相覆盖 - 对
POST/PUT默认跳过缓存,除非业务明确幂等(比如带 id 的查询式 POST) - 写入缓存前检查
rw.statusCode,只缓存 200/304/404 等可复用状态码,跳过 5xx 或重定向
ristretto 是目前最省心的本地缓存选择
它不是“框架”,但解决了 sync.Map 和手写 lru.Cache 在生产环境的全部硬伤:无锁读、cost-aware 驱逐、懒过期、自动后台清理(可关)、支持 OnEvict 回调。
- 初始化时别设
NumCounters: 1e7—— 小对象会撑爆内存,改用MaxCost: 100 * 1024 * 1024(100MB)更稳 -
Set时传ristretto.TTL,底层自动处理过期,不用自己存时间戳、也不用在Get里做time.Now().Before(expireAt)判断 - 它不依赖访问时间排序淘汰,而是基于采样估算“热度”,对大对象友好,也避免了时间判断带来的锁竞争
- 如果缓存的是 JSON 响应体,
value类型建议用[]byte,避免反复json.Marshal和json.Unmarshal
Cache-Control 头必须和服务端缓存逻辑对齐
浏览器和 CDN 是否复用响应,只看这个头,跟你的内存里有没有数据完全无关。Go 默认不设它,等于告诉所有人“别缓存”。
立即学习“go语言免费学习笔记(深入)”;
- 公开数据(如地区列表、静态配置):用
w.Header().Set("Cache-Control", "public, max-age=300")(5 分钟) - 用户私有数据(如个人资料):必须用
"private, max-age=60",禁止 CDN 缓存,但允许浏览器缓存 - 含
Cookie或Authorization的请求,绝对不能返回public—— 否则 A 用户的响应可能被缓存并返回给 B - 配合
ETag减少带宽:在缓存写入时计算fnv.New64a().Write(body).Sum(nil)存为ETag;收到If-None-Match时先比对再决定返回 200 还是 304,且必须在进入业务逻辑前完成,否则 DB 查询已执行,失去意义
最易被忽略的点是缓存键的粒度和过期策略的耦合——同一个接口,用户列表和用户详情的 TTL 应该不同,但很多人用统一 key 前缀+统一 TTL,导致高频变动数据长期滞留或低频数据频繁穿透。真正的高效,来自对每个缓存项生命周期的独立控制,而不是“一缓全缓”。


















