结论:95%场景用github.com/patrickmn/go-cache就够了,它默认并发安全、清理稳定、API简洁;自研易出错因Get时漏删、sync.Map无法遍历清理、无后台goroutine致内存泄漏,或手动扫map引发竞态与资源浪费。

直接说结论:别自己封装“带过期的 map”,95% 的场景用 github.com/patrickmn/go-cache 就够了,它默认并发安全、清理逻辑稳定、API 简洁,且不强制你处理类型断言以外的额外负担。
为什么自研 map + time.Now() 过期检查容易出错
常见错误现象是「缓存看起来没过期,但 Get 返回空」或「内存持续上涨,过期项从不被删」。根本原因是:只在 Get 时检查 expireAt.Before(time.Now()),却没配对做 Delete;或者用了 sync.Map 但无法遍历清理——它不支持 Range 以外的批量操作,而 Range 期间不能安全 Delete。
使用场景上,这种手写方案只适合极简 PoC 或教学演示,一旦加压(比如 QPS > 500)、加字段(比如要支持 LRU 淘汰)、加监控(比如想看命中率),就得反复补锁、加 goroutine、修竞态,很快失控。
- 每次
Get都调用time.Now()—— 在高频访问下是小开销,但叠加锁竞争后会放大延迟 - 没有后台清理 goroutine:过期项永远滞留,直到下次
Get触发删除,导致内存不可控 - 若手动启 goroutine 定期扫 map,又得自己处理 stop 信号、防止重复启动、避免清理时写冲突
go-cache 的参数含义和典型误用
go-cache 初始化时两个 time.Duration 参数常被误解:New(defaultExpiration, cleanupInterval)。
立即学习“go语言免费学习笔记(深入)”;
defaultExpiration 是 set 时不显式传 TTL 时的默认值(比如 cache.DefaultExpiration 表示永不过期);cleanupInterval 是后台 goroutine 多久扫一次过期项——它不是「精确过期时间」,只是清理节奏。
- 设成
0:禁用自动清理,所有过期逻辑退化为 Get 时惰性删除 → 内存泄漏风险高 - 设太小(如
100 * time.Millisecond):goroutine 频繁唤醒,CPU 毛刺明显,尤其在低负载时无必要 - 推荐值:业务能容忍的过期延迟上限的 2–3 倍,例如允许最多 5 秒过期数据残留,就设
10 * time.Second
注意:它返回的 value 是 interface{},必须显式类型断言,比如 v, ok := c.Get("key").(*User);断言失败不会 panic,但 ok == false,容易漏判。
什么时候该换别的库,而不是硬改 go-cache
当出现以下任一情况,说明你已超出 go-cache 的设计边界:
- 缓存项超过 10 万,且单条 bigcache,但它需自行管理 TTL(比如把
expireAt打包进 value 序列化) - 需要 LRU 淘汰 + TTL 过期双机制共存 →
TTLCache或kcache更合适,前者泛型安全,后者支持「过期后自动回源」的钩子函数 - 要求毫秒级精确过期(比如金融风控 token)→ 别用任何基于定时扫描的库,改用时间轮(
timingwheel)或 Redis
强行给 go-cache 加 LRU 或事件回调,等于重写一半源码,还失去升级能力。
泛型缓存封装的坑:别在接口层暴露 interface{}
有人想“统一 API”,定义一个泛型接口再套一层 go-cache,结果发现:只要底层用 map[string]interface{} 存值,泛型就只剩语法糖,运行时仍要断言;而真要用泛型约束类型,就得放弃 go-cache 的内部结构,自己实现存储和清理——又回到第一段说的问题。
真正轻量又类型安全的做法,是用 TTLCache 这类原生泛型库,或者接受 go-cache 的简单性,把类型断言逻辑收在 service 层(比如封装成 GetUser(id string) (*User, bool)),而不是试图在缓存抽象层解决所有问题。
最易被忽略的一点:所有基于内存的本地缓存,过期时间都是「进程内」视角。如果服务多实例部署,各实例缓存不同步,TTL 只影响本机行为——这不是 bug,是特性。需要全局一致过期语义,必须上 Redis 或其他分布式协调机制。


















