不推荐用 sync.map + 时间戳自实现过期缓存,因缺乏原子性读-判-删操作,易致竞态与缓存穿透;github.com/patrickmn/go-cache 是最省心的选择,它基于 map+sync.rwmutex,支持 ttl 设置、惰性过期与后台定期清理。

用 sync.Map + 时间戳自己管理过期是否可行?
可行,但容易出错。核心问题是 sync.Map 不提供原子性地「读+判断过期+删除」操作,多个 goroutine 并发访问时可能读到已过期却未清理的值。更麻烦的是,你得在每次 Load 后手动检查 time.Now().After(expireTime),漏掉一次就缓存穿透。不推荐从零手写,除非你明确需要细粒度控制且愿意承担维护成本。
github.com/patrickmn/go-cache 是最省心的选择吗?
对大多数本地缓存场景来说,是。它内部用 map + sync.RWMutex,支持带 time.Duration 的 Set、自动后台清理(默认每分钟扫描一次)、可选的 OnEvicted 回调。注意两点:
– 它不区分「绝对过期时间」和「相对 TTL」,所有过期都按写入时的 time.Now() + ttl 计算;
– 如果你设了 5 分钟 TTL,但某条数据写入后 10 分钟才首次被访问,它仍会返回(只要没被后台清理掉),即「惰性过期」而非「主动失效」。
示例用法:
import "github.com/patrickmn/go-cache"
c := cache.New(5*time.Minute, 10*time.Minute) // 默认过期 + 清理间隔
c.Set("user:123", &User{ID: 123}, cache.DefaultExpiration)
val, found := c.Get("user:123") // 返回 interface{},需类型断言
为什么不用 bigcache 或 freecache?
它们面向高吞吐、大容量场景,主打减少 GC 和内存碎片,但默认不支持 per-entry 过期。虽然 bigcache 可通过封装实现 TTL(比如把过期时间塞进 value 里,每次 Get 时检查),但这增加了序列化/反序列化开销,也绕过了它原本的 zero-allocation 设计优势。如果你的缓存项少于 10 万、单条数据小于 1KB、QPS 在几千以内,用它们反而增加复杂度,得不偿失。
自研简单版:用 map + time.Timer 能精准控制过期吗?
不能。为每个 key 启一个 time.Timer 会导致 goroutine 泛滥(百万 key ≈ 百万 goroutine),且 Timer 无法取消后复用。正确做法是只用一个后台 goroutine 驱动优先队列(如最小堆)管理过期时间,或直接复用 go-cache 的清理逻辑。真正需要「毫秒级精确过期」的场景极少,绝大多数业务能接受 10 秒内的延迟清理——这时候,别自己造轮子,老老实实用经过验证的库。
过期逻辑本身不难,难的是并发安全、内存增长控制、清理时机权衡。哪怕只是加个 ttl 参数,背后涉及的边界条件(如系统时间回拨、长时间 GC STW 导致清理滞后)都容易被忽略。











