绝大多数场景下应优先使用带 sync.RWMutex 的普通 map 而非 sync.Map,因其读无锁、写仅锁、性能更稳;sync.Map 仅适合大量 goroutine 频繁读偶发写的特定场景,且不支持遍历、TTL 或原子清空。

用 sync.Map 还是自己加锁的 map?
绝大多数场景下,直接用 sync.Map 反而更慢、更占内存,尤其当读写比高且 key 数量稳定时。它专为「大量 goroutine 频繁读、偶发写」设计,不是通用缓存替代品。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 如果缓存 key 总数可控(比如几百到几万),优先用带
sync.RWMutex的普通map—— 读并发无锁开销,写时才锁,性能更稳 -
sync.Map不支持遍历、不保证迭代顺序、无法原子性清空,想做 TTL 或定期淘汰基本没法搞 - 别被 “并发安全” 四个字骗了:高频写 + 小数据量时,
sync.Map的内部分片和原子操作反而拖累性能
怎么加 TTL(过期时间)又不卡主线程?
用定时器逐个检查 key 是否过期?那会阻塞或漏删。真正在生产里跑得动的方案,是「惰性删除 + 后台 goroutine 定期扫描」结合。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 每次
Get时顺手检查expireAt字段,过期就删掉并返回空 —— 成本低、无额外 goroutine - 另起一个 goroutine,用
time.Ticker每 30 秒扫一次(频率按业务容忍度调),只随机采样 1% 的 key 做清理,避免全量遍历卡住 - 别用
time.AfterFunc给每个 key 起定时器:key 多了会创建海量 timer,GC 压力大,还容易泄漏
要不要用第三方库比如 freecache 或 bigcache?
它们确实比原生 map 更省内存、支持更大容量,但代价是:接口更重、行为更隐蔽、调试更难。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 单机缓存 map + 自定义结构体就够了,省心且可控
-
freecache内部用 ring buffer,写满后自动淘汰老数据,但不支持精确过期;bigcache用分片 + 时间戳数组,TTL 是整数秒粒度,且所有 value 必须是 []byte - 一旦用了这些库,出问题时你得看懂它的分片逻辑、内存布局、哈希冲突处理——比自己写的几行锁逻辑难 debug 得多
缓存击穿/雪崩怎么在本地层就挡住?
本地缓存本身不解决分布式场景下的击穿或雪崩,但它可以配合简单策略降低下游压力。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 对热点 key 加
singleflight.Group:同一时刻只放一个 goroutine 去加载,其余等待结果,避免 DB 被打爆 - 设置「逻辑过期」而非绝对过期:value 里存
expireAt和data,过期后允许再用 5 秒旧值,同时异步刷新,保证可用性 - 别依赖本地缓存扛住突发流量——它没容量限制、没降级开关、没监控埋点,真出事时你连“缓存是不是挂了”都难确认
本地缓存最常被忽略的点,是它根本不是独立服务,而是代码里一段容易膨胀、难以观测、升级时经常忘掉的逻辑。上线前至少得确认三件事:map 锁粒度是否合理、expireAt 类型有没有用错 time.UnixNano()、goroutine 清理任务有没有被 defer 掉导致没启动。


















