应选用 github.com/hashicorp/golang-lru 而非手写 lru,因其经生产验证、线程安全、支持容量限制与驱逐回调;手写易在并发、淘汰逻辑和生命周期管理上出错。

为什么不用自己写 lru.Cache 而要选 github.com/hashicorp/golang-lru
Go 标准库没有内置 LRU 实现,container/list + map 手写容易出错,尤其在并发、淘汰逻辑、键值生命周期上。官方推荐的第三方库 github.com/hashicorp/golang-lru 经过大量生产验证,支持线程安全、带容量限制、可定制驱逐回调。
常见错误现象:map 直接配 list 实现时,忘记加锁导致 panic;或在 Get 后没把节点移到头导致“热数据”被误删;还有人用 time.Now() 做访问时间排序——这既不准又慢。
- 使用场景:HTTP 响应缓存、数据库查询结果暂存、配置项热加载兜底
-
lru.New(128)创建固定容量缓存,超出自动淘汰最久未用项 - 如果需要带 TTL,得用
lru.NewWithEvict配合自定义回调清理过期资源(比如关闭文件句柄) - 性能影响:纯内存操作,单次
Get/Put是 O(1);但开启OnEvicted回调后,淘汰开销取决于回调本身
如何让 lru.Cache 支持并发安全
默认 lru.Cache 不是并发安全的,多 goroutine 同时 Get 或 Put 会 panic:fatal error: concurrent map read and map write。
解决办法不是加一层 sync.RWMutex 包裹整个 cache——那样会严重串行化,吞吐掉一半以上。正确做法是直接用 lru.NewARC 或更常用的 lru.New2Q,它们内部已做细粒度锁优化;或者用 github.com/hashicorp/golang-lru/simplelru 的线程安全封装版 lru.NewThreadSafe(128)。
-
lru.NewThreadSafe(128)内部用sync.RWMutex锁住 map 和 list 操作,比手动包一层更轻量 - 不要对
lru.Cache做 double-check lock:它不提供TryGet这类非阻塞接口,强行封装反而增加竞态风险 - 如果你用的是 Go 1.21+,且缓存 key 是字符串/整数等可比较类型,考虑用
sync.Map+ 时间戳做简易 LRU——但仅限低频、容忍误差的场景
Put 和 Get 返回值里那个 bool 到底代表什么
很多人以为 cache.Get(key) 返回的 bool 表示“是否命中”,其实它只表示“是否成功取出值”,和“是否命中”不是完全等价的——比如你用 OnEvicted 回调主动删掉某个 key,再 Get 它就会返回 (nil, false);但如果 key 存在但值本身就是 nil(比如缓存了 *int),Get 也会返回 (nil, true)。
- 典型误用:
if v, ok := cache.Get(k); !ok { /* 缓存未命中 */ }——这个判断在值允许为nil时不可靠 - 正确做法:把值类型设计为非指针(如
int而非*int),或统一包装成struct{ Val T; Valid bool } -
Put的bool返回值表示“是否因容量满而淘汰了旧条目”,不是“是否插入成功”——即使 key 已存在,Put仍会更新值并返回true(只要触发了淘汰)
为什么缓存命中率低?检查这三个地方
上线后发现 cache.Len() 一直接近 0,或 cache.Keys() 返回空 slice,大概率不是算法问题,而是 key 构造或生命周期出了岔子。
- key 是 struct?确保它实现了
Equal方法或字段可比较;否则 map 查找失败,每次Put都当新 key 处理 - key 含指针或 slice?Go 的 map key 要求可比较,
[]byte、map[string]int这类不能直接当 key,得转成string或[32]byte - 缓存对象本身含指针或未导出字段?
lru.Cache不深拷贝值,如果外部修改了原始对象,缓存里的副本也会变——这不是 bug,是预期行为
复杂点在于:LRU 本身不关心业务语义,它只管“谁最近被用了”。真正决定命中率的,是你怎么定义 key、怎么控制对象生命周期、以及是否在该清缓存的地方忘了调用 Remove 或 Purge。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











