sync.map是go标准库中专为高并发读多写少场景设计的线程安全map,通过read/dirty双map结构和分片锁实现高效并发,支持store、load、loadorstore、delete等方法,但不支持len()或range遍历,亦无内置ttl机制,需自行封装过期逻辑。

用 sync.Map 实现线程安全的内存缓存
Go 标准库的 sync.Map 是专为高并发读多写少场景设计的,比手动加锁的 map 更轻、更省心。它不支持遍历或获取长度,但对缓存这种“查+偶尔设”的模式正合适。
常见错误是直接拿 sync.Map 当普通 map 用:比如想用 len(m) 统计条数,或者用 for k, v := range m 遍历——这两者都不支持,会编译失败或 panic。
- 设值用
m.Store(key, value),不是m[key] = value - 取值用
value, ok := m.Load(key),返回两个值,ok必须检查 - 删除用
m.Delete(key),没有delete(m, key)这种写法 - 如果需要 TTL(过期时间),
sync.Map不提供,得自己在 value 里嵌入时间戳并配合定时清理 goroutine
给缓存加 TTL:用 time.AfterFunc 还是后台 goroutine?
标准库没内置带过期的缓存,硬加 time.AfterFunc 到每个 Store 调用里看似简单,但大量短生命周期 key 会创建海量 timer,触发 GC 压力和调度开销——这是高频踩坑点。
更稳的做法是启动一个单独的清理 goroutine,定期扫描(比如每秒一次),只对已过期的 key 调用 Delete。扫描时用 Range 方法遍历,注意它不保证原子性,可能漏删或重复删,但对缓存这种最终一致场景可接受。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 别在
Store里起 goroutine 调用time.Sleep,容易积压协程 - value 类型建议定义为结构体:
type cacheItem struct { data interface{}; expiresAt time.Time } - 扫描间隔不宜太短(5s),视业务容忍延迟而定
什么时候该换第三方库,比如 gocache 或 ristretto?
当你的缓存开始出现以下任一情况,就该考虑换库了:频繁命中率骤降、内存占用持续上涨、GC pause 明显变长、需要 LRU/LFU 驱逐策略。
gocache 轻量易用,适合小项目,但底层仍是 map + RWMutex,高并发下性能不如 ristretto;ristretto 是纯内存、无锁、近似 LFU 的高性能缓存,但 API 稍重,且不内置序列化——如果你要存 struct,得自己 json.Marshal 再存。
- 单机、QPS sync.Map + 自研 TTL 足够
- 需要驱逐策略或监控指标(hit/miss ratio)→ 上
ristretto - 只是想快速加个带 TTL 的缓存,不care性能极限 →
gocache可以省半天事
测试缓存是否真的线程安全?别只测“不 panic”
很多人写个 for 循环并发 Store 和 Load,看到没 panic 就认为 OK。其实漏掉了关键验证:数据一致性。比如两个 goroutine 同时 Store 同一个 key,最终值是否是后者?sync.Map 保证最后写入生效,但你得测出来,而不是靠文档信任。
推荐用 go test -race 跑并发测试,同时用断言检查返回值是否符合预期。特别注意 LoadOrStore 行为:它返回的是“本次操作实际加载到的值”,不是“是否新存入”的布尔值——这个语义容易误解。
- 测试用例里至少包含:并发写同 key、并发读写同 key、混合 Load/Store/Delete
- 别用
fmt.Println打日志代替断言,race detector 不捕获 log 输出 - 如果用了自研 TTL 清理,测试时记得 mock
time.Now(),否则时间不可控
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










