直接用 http.Transport 做代理缓存会失败,因其默认不缓存响应,且 RoundTripper 接口无法拦截并缓存响应体,多次读取 resp.Body 会导致“read on closed response body”错误。

为什么直接用 http.Transport 做代理缓存会失败
Go 标准库的 http.Transport 默认不缓存响应,即使你设置了 Cache-Control: public, max-age=3600,它也不会自动复用。更关键的是,http.RoundTripper 接口不暴露原始请求头/响应体,无法在中间拦截并写入本地缓存。常见错误是试图在 RoundTrip 方法里读取 resp.Body 两次——第一次读完后 Body 就被关闭,第二次读会得到空内容或 http: read on closed response body 错误。
- 必须在读取响应前,用
io.TeeReader或io.MultiReader复制原始字节流 - 缓存键不能只依赖
req.URL.String(),要归一化:去掉utm_*参数、忽略查询参数顺序、统一协议和 host 大小写 -
Cache-Control中的no-store、no-cache、private必须严格跳过缓存
用 http.ServeHTTP 实现可缓存的反向代理
标准库 httputil.NewSingleHostReverseProxy 是起点,但它不支持缓存逻辑注入。你需要继承并重写 Director 和响应处理流程。核心是在 proxy.ServeHTTP 调用后,立即拦截 ResponseWriter,捕获状态码、头信息和响应体。
- 用
httptest.NewRecorder()拦截响应,再用bytes.Buffer存储原始 body - 缓存键生成示例:
fmt.Sprintf("%s %s %s", req.Method, normalizeURL(req.URL), req.Header.Get("Accept")) - 缓存过期时间优先取
Cache-Control: max-age=N,其次 fallback 到Expires头
func (c *cachedProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
key := c.cacheKey(req)
if entry, ok := c.cache.Get(key); ok && !entry.Expired() {
entry.WriteTo(rw)
return
}
resp := httptest.NewRecorder()
c.proxy.ServeHTTP(resp, req)
buf := &bytes.Buffer{}
io.Copy(buf, resp.Body)
bodyBytes := buf.Bytes()
cacheEntry := &cacheItem{
StatusCode: resp.Code,
Header: resp.Header().Clone(),
Body: bodyBytes,
CreatedAt: time.Now(),
TTL: c.ttlFromHeaders(resp.Header()),
}
c.cache.Set(key, cacheEntry, cacheEntry.TTL)
rw.WriteHeader(resp.Code)
for k, vs := range resp.Header() {
for _, v := range vs {
rw.Header().Add(k, v)
}
}
rw.Write(bodyBytes)
}
bigcache 或 ristretto 选哪个做内存缓存
bigcache 适合高并发、大量小响应(如 JSON API),因为它用分片 map + 时间戳淘汰,避免 GC 压力;但不支持基于 TTL 的自动过期,得自己轮询清理。ristretto 支持精确 TTL、LRU+LFU 混合策略,更适合混合大小响应(含图片、HTML),但内存占用略高、初始化稍慢。
- 如果代理目标主要是 REST API(平均响应 bigcache.NewBigCache,设置
Shards: 128 - 如果需缓存 HTML 或压缩资源(可能 >100KB),用
ristretto.NewCache,开启OnExit: func(key uint64, value interface{})清理大对象 - 避免用
sync.Map:它不支持容量限制和淘汰,长期运行必然 OOM
如何处理 POST/PUT 请求的缓存穿透
GET 请求天然幂等,可安全缓存;但 POST/PUT 默认不可缓存。若业务明确某些 POST 是只读操作(如 /api/search),需手动放行。关键是识别“语义 GET”:检查路径是否含 search、query、list,且请求体是 JSON 查询条件而非业务变更数据。
立即学习“go语言免费学习笔记(深入)”;
- 禁止缓存
Content-Type: application/json且 body 含"update"、"delete"、"status": "active"等字段的请求 - 对放行的 POST,缓存键必须包含
sha256(body),而非仅 URL —— 否则不同查询参数会命中同一缓存 - 永远不缓存带
Cookie或Authorization头的请求,除非显式配置CacheableHeaders: ["X-User-ID"]
缓存系统最难的不是存和取,而是判断“该不该缓存”——这个决策点分散在 URL 归一化、头解析、body 检查、TTL 计算多个环节,漏掉任意一个都可能把私有数据缓存成公共响应。










