缓存逻辑必须置于handler外层作为中间件,统一处理key生成、读写及TTL控制;禁用仅缓存成功响应、忽略状态码、滥用sync.Map等错误做法。

缓存逻辑必须放在 handler 外层,不能塞进业务函数里
Go 的 HTTP handler 本身是无状态的,但缓存需要跨请求共享数据。如果把 cache.Get / cache.Set 写在业务逻辑函数内部(比如 getUserByID),会导致缓存无法复用——因为每次调用都走新上下文,且没做 key 统一或过期控制。
正确做法是把缓存作为中间件或 wrapper 套在 handler 上,统一拦截请求、生成 key、读写缓存。常见错误是:只缓存成功响应,却忽略 404 或 500 状态码的缓存策略,结果反复穿透到后端。
- key 必须包含 method + path + query string(用
req.URL.String()安全但注意 URL 解码差异) - 对 POST/PUT 请求,若要缓存,需额外序列化
req.Body并参与 key 计算(通常不推荐) - 缓存 value 应该是完整
*http.Response的序列化结果(含 status code、headers、body),而非仅 body 字节
用 sync.Map 做本地缓存时要注意 GC 和内存泄漏
sync.Map 适合读多写少、key 数量可控的场景,但它是无过期机制的。直接用它当接口缓存,很容易积累大量 stale 数据,尤其当请求带动态参数(如 /user?id=123、/user?id=456)时,key 不可复用,Map 持续膨胀。
更稳妥的做法是封装一层带 TTL 的本地缓存,比如用 time.Now().UnixNano() 存入 value,并在 Get 时比对。别依赖 sync.Map 自动清理——它不会删过期项。
立即学习“go语言免费学习笔记(深入)”;
type CacheItem struct {
Data []byte
Expires int64 // Unix nanos
}
func (c *LocalCache) Get(key string) ([]byte, bool) {
if v, ok := c.m.Load(key); ok {
item := v.(CacheItem)
if time.Now().UnixNano() < item.Expires {
return item.Data, true
}
c.m.Delete(key) // 主动清理
}
return nil, false
}
用 Redis 实现分布式缓存时,key 设计要防哈希冲突
多个服务实例共用一个 Redis,key 必须全局唯一且可预测。常见错误是直接用 req.RequestURI 当 key,但不同客户端可能发送带空格、未编码的 URL,Redis 中会视为不同 key;或者忽略 header 差异(如 Accept: application/json vs application/xml),导致缓存错乱。
建议标准化 key 生成逻辑:
- 对 path 和 query 使用
url.PathEscape和url.QueryEscape - 若需区分 content-type,把
req.Header.Get("Accept")哈希后截取前 8 位拼入 key - 加固定前缀如
"api:v2:",便于 redis-cli 批量清理 - 避免在 key 中放用户 ID 等敏感字段(除非已脱敏或加密)
缓存失效不是“删掉就完事”,得考虑并发击穿
当某个热点接口缓存过期瞬间,大量请求同时穿透到后端,可能打挂 DB 或下游服务。单纯用 DEL key 触发失效,没有保护机制。
可行方案有二:
- 使用「逻辑过期」:value 里存一个
expire_at字段,缓存未物理删除,get 时发现逻辑过期则触发异步回源更新(用redis.SetNX抢锁) - 预热 + 延迟双删:在定时任务中提前 30s 刷新即将过期的 key;更新 DB 后,先删缓存 → 写 DB → 延迟几百毫秒再删一次缓存
无论哪种,都要监控 cache.miss_rate 和 backend.qps,否则失效策略是否生效根本没法验证。










