不能靠container/list+map实现可靠ttl,因其节点无时间戳、不支持按时间淘汰,仅靠get时惰性检查会残留脏数据,全量扫描又破坏o(1)复杂度,且并发下易出现竞态与内存泄漏。

Go 语言里没有开箱即用的“带过期的 LRU 缓存”,必须自己组合或选对库——直接用 sync.Map 或手撸 container/list + map 无法自动处理过期,容易漏判、并发错乱、内存泄漏。
为什么不能靠标准库 container/list + map 实现可靠 TTL
标准库的 container/list 只管访问顺序,map 只管键值映射,两者都不存时间戳,也不提供过期检查入口。你得手动:
- 每次
Get都要查时间戳并判断是否过期,否则返回脏数据 - 每次
Set都要写入time.Now().Add(ttl),且确保结构体字段可序列化 - 没加锁就并发读写
map,会 panic;加了锁又可能阻塞高频Get - 不主动清理,过期项永远留在内存里——哪怕 LRU 已淘汰它,时间戳也还在 map 中残留
这不是“少写几行”的问题,是逻辑耦合紧、边界难覆盖、测试成本高的典型陷阱。
go-cache 是最省心的入门选择:自动 janitor 清理 + 线程安全
github.com/patrickmn/go-cache 封装了后台 goroutine(janitor)定时调用 DeleteExpired(),不用你操心触发时机。关键配置只有两个参数:
-
defaultExpiration:设为5 * time.Minute,后续Set("k", v, cache.DefaultExpiration)就自动生效 -
cleanupInterval:建议设为defaultExpiration / 2左右(比如 2.5 分钟),太短增加 CPU 开销,太长导致过期项滞留久
示例初始化:
cache := cache.New(5*time.Minute, 2*time.Minute)
注意:NoExpiration 是常量 -1,DefaultExpiration 是常量 0,别传错数值类型。
想用 LRU + TTL?用 github.com/hashicorp/golang-lru/v2 + 手动过期检查
如果业务需要严格 LRU 淘汰顺序(比如缓存结果必须按访问频次/时序淘汰),go-cache 的 FIFO-like 清理不满足要求,就得上 lru.Cache。但它不内置 TTL,必须自己包一层:
- value 类型必须含
ExpireAt time.Time字段,例如type CacheItem struct { Data interface{}; ExpireAt time.Time } -
Get(key)里必须做if item.ExpireAt.Before(time.Now()) { return nil, false }判断,不能只查 key 是否存在 -
Set(key, value)时只更新时间戳,不要在 set 时主动删过期项——LRU 容量控制交给它自己 - 所有时间比较统一用
time.Now().After(expireAt),避免Sub().Seconds()引入浮点误差
别试图给 lru.Cache 加定时扫描 goroutine——每个 key 启一个 time.AfterFunc 在万级 key 下直接压垮调度器。
高并发场景下,过期检查和清理的性能敏感点
真正压测时暴露的问题往往不在“怎么写”,而在“什么时候判”:
- 后台 janitor 清理(如
go-cache)是周期性全量扫描,key 超过 10 万后单次DeleteExpired()可能卡住几十毫秒,影响写操作延迟 - 惰性检查(如
lru.Cachewrapper)把压力摊到每次Get,但若大量 key 集中过期,会引发“缓存击穿”——瞬间大量请求穿透到下游 - 用
sync.RWMutex包裹 map 实现的简单缓存,读多写少时没问题;但一旦有高频写(比如每秒千次Set),写锁争抢会让Get明显变慢
没有银弹方案:小项目用 go-cache 省事;对淘汰策略敏感或 QPS 过万的服务,得压测验证 janitor 间隔或改用 ristretto 这类更底层可控的库——但那就不再是“结合语言学习”的范畴了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











