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

为什么不用 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.RWMutex或sync.Mutex锁住整个操作序列 - 删除最久未用项时,别只删
list节点,一定要同步从map中delete,否则 map 持有指针造成内存泄漏
测试缓存是否真的生效
光跑通 Set/Get 不够,重点验证边界行为:
Yes!Sun基于PHP+MYSQL技术,体积小巧、应用灵活、功能强大,是一款为企业网站量身打造的WEB系统。其创新的设计理念,为企业网的开发设计及使用带来了全新的体验:支持前沿技术:动态缓存、伪静态、静态生成、友好URL、SEO设置等提升网站性能、用户体验、搜索引擎友好度的技术均为Yes!Sun所支持。易于二次开发:采用独创的平台化理念,按需定制项目中的各种元素,如:产品属性、产品相册、新闻列表
立即学习“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 时手动检查时间戳再决定是否返回。









