Golang开发一个内存缓存小项目实践

11次阅读

sync.map 不适合作为通用缓存,因其无过期机制、无容量限制、遍历无序且缺乏原子的“读+过期检查+删除”操作;推荐使用 go-cache,它轻量、线程安全、支持 TTL 和定时清理。

Golang开发一个内存缓存小项目实践

为什么不用 sync.Map 直接当缓存用

很多人一想“内存缓存”,立刻写 sync.Map 存键值,但实际会踩坑:sync.Map 没有过期机制、不支持容量限制、遍历时无法保证顺序,更关键的是——它不提供原子的“读+过期检查+删除”组合操作。真实缓存需要 Get 时自动剔除过期项,否则缓存会持续膨胀、返回脏数据。

github.com/patrickmn/go-cache 快速落地

这个库轻量(单文件)、线程安全、支持 TTL 和清理策略,比自己手撸更可靠。注意它不是 LRU,而是基于定时器 + 延迟删除(lazy eviction),适合中小规模高频读、低频写的场景。

  • 安装:go get github.com/patrickmn/go-cache
  • 初始化时建议显式设置默认过期时间,避免漏传导致永不过期:
    cache := cache.New(5*time.Minute, 10*time.Minute)

    (第一个参数是 item 默认 TTL,第二个是清理 goroutine 执行间隔)

  • 存值时可覆盖默认 TTL:cache.Set("user:123", userObj, cache.DefaultExpiration)cache.Set("Token:abc", "xxx", 30*time.Second)
  • 取值要判空:if x, found := cache.Get("key"); found { ... }found == false 不代表 key 不存在,可能是已过期被逻辑删除(还没被清理 goroutine 物理清除)

自定义简单 LRU 缓存要注意的三个点

如果真要自己写(比如规避依赖、或需要精确 LRU 行为),别直接套用 container/list + map 教程代码——它们通常忽略并发和内存泄漏风险。

  • map 的 key 类型必须可比较(不能是 slice、map、func),常见错误是用 []byte 当 key,得转成 String 或用 unsafe.String 避免拷贝
  • LRU 的“访问更新”必须原子:先从 list 中移除节点,再 PushFront,中间不能被其他 goroutine 插入导致 panic;必须用 sync.RWMutexsync.Mutex 锁住整个操作序列
  • 删除最久未用项时,别只删 list 节点,一定要同步从 mapdelete,否则 map 持有指针造成内存泄漏

测试缓存是否真的生效

光跑通 Set/Get 不够,重点验证边界行为:

立即学习go语言免费学习笔记(深入)”;

  • 过期后 Get 返回 nil, false(不是 nil, true
  • 并发写同一 key 时不会 panic(go-cache 内部用 sync.RWMutex,自己写的必须测)
  • 大量写入后内存增长平稳(用 runtime.ReadMemStats 对比前后 Alloc 字段)
  • 故意设极短 TTL(如 100* time.Millisecond),用 time.Sleep 后再 Get,确认命中率为 0

真正难调的不是存取逻辑,是过期时机和清理节奏的配合——尤其在高并发下,清理 goroutine 可能滞后,导致短暂返回过期值。生产环境建议加一层包装,Get 时手动检查时间戳再决定是否返回。

text=ZqhQzanResources