直接缓存外部api响应需在writeheader前拦截响应并序列化存redis,key须包含method、url、headers等全部影响因子,空响应和404也须缓存以防穿透,推荐用json.rawmessage存取并原子设置ttl。

直接缓存外部 API 响应是 Gin 项目里最常见、也最容易出错的缓存场景。别指望加个 Cache-Control 头就完事——那只是告诉浏览器“你可以缓存”,服务端根本没存任何东西。真正要落地,得自己拦截响应、序列化、存 Redis 或内存,并在下次请求时提前返回。
缓存中间件必须在 WriteHeader 前介入
一旦调用过 w.WriteHeader() 或往 w 写了字节,HTTP header 就被冻结,后续设置 Cache-Control 或替换 body 都无效。所以缓存逻辑只能发生在 handler 执行前或执行中、但写响应之前。
- 正确做法:用包装型
http.ResponseWriter实现拦截,重写WriteHeader和Write方法,在第一次Write时捕获完整 body - 错误写法:在 handler 里手动调用
w.WriteHeader(200),这会让缓存中间件收不到状态码和 body - 连
http.Error(w, ...)都会触发WriteHeader,所以错误路径也得走同一套包装逻辑,否则 4xx/5xx 响应永远不进缓存
缓存 key 必须包含影响响应的所有变量
外部 API 的响应往往依赖 query、header 甚至 body(如 POST 查询),漏掉任一维度,就可能把用户 A 的数据返回给用户 B。
- 基础 key 构成:
method:GET|url:/api/forecast?city=beijing|accept:application/json|auth:token_abc - 对无认证的公开接口,至少含
URL.Path+r.URL.RawQuery;带Authorization或Cookie的,必须 hash 后拼入 key - 避免用
r.Header.Get("User-Agent")这类高基数字段,会导致 key 泛滥;真要区分移动端,用自定义 header 如X-Client-Type: mobile - 如果外部 API 返回时间戳字段(如
"updated_at": "2026-08-11T15:30:00Z"),缓存前先归一化为固定值(如"updated_at": "cached"),否则每次响应都“不同”
Redis 存什么、怎么存才安全
Go 里往 Redis 存响应体,不是直接塞 struct{} 或指针——反序列化时容易 panic,且无法跨进程复用。
- 推荐存
[]byte或json.RawMessage:它们是稳定序列化结果,读取后可直接json.Unmarshal到目标结构体 - 用
SET key value EX 300而非SETEX:前者兼容性更好,且能原子性设置过期,避免SET+EXPIRE两步导致的竞态 - 不要用
sync.Map存响应体:它不支持 TTL,缓存无限增长;短周期测试可以,生产环境必须上 Redis - 若用
go-redis客户端,注意client.Get(ctx, key).Bytes()返回的是拷贝,Bytes()后原值仍可复用,不用额外copy
缓存穿透和空响应怎么处理
外部 API 返回 404 或空数组时,如果不缓存,攻击者反复刷不存在的 ID 就会打穿你的下游服务。
- 即使响应 body 为空或 status code 是 404,也要缓存,但过期时间设短(如
5 * time.Minute) - key 可统一加前缀
extapi:miss:,便于后台扫描清理 - 对高频查询的固定参数(如
/api/status),启动时用 goroutine 预热一次,避免首屏雪崩 - 别依赖下游 API 的
Cache-Control头做决策——它只约束 CDN/浏览器,你自己的缓存策略得独立控制
最易被忽略的点:缓存键里漏了 Accept 头。同一个 URL,Accept: application/json 和 Accept: text/plain 应该命中不同缓存项,否则 JSON 响应可能被当纯文本返回给客户端。
大量免费API接口:立即使用
涵盖生活服务API、金融科技API、企业工商API、等相关的API接口服务。免费API接口可安全、合规地连接上下游,为数据API应用能力赋能!











