sync.Map 无法直接实现过期功能,因其不支持自动 TTL 清理;需手动在 Get 时检查时间戳并用 LoadAndDelete 原子清理,避免竞态与重复删除。

为什么不能直接用 sync.Map 实现过期功能
sync.Map 本身不支持键值对的自动过期,它只提供并发安全的增删查,没有 TTL(time-to-live)机制。你调用 Load、Store 或 Delete 都是手动触发的,不会因为时间到了就自动清理。如果直接把过期时间存在 value 里,每次 Load 都得手动检查时间戳并可能触发 Delete,但这会引入竞态:两个 goroutine 同时 Load 到已过期项,都尝试 Delete,虽不 panic,但逻辑冗余且无法保证“恰好一次清理”。
如何让过期检查和删除真正并发安全
关键是在读取时做原子判断,并配合 LoadAndDelete 或 CAS 式重试。推荐组合:sync.Map 存储 struct{ value interface{}; expireAt time.Time },每次 Load 后立即比对 expireAt.Before(time.Now());若过期,用 LoadAndDelete 尝试移除——它天然原子,即使另一个 goroutine 同时删掉,返回值也能帮你判断是否“成功摘除”。
- 不要在
Store时预删旧 key:sync.Map不保证Load和Delete之间无插入,可能刚删完就被新写入覆盖 - 避免用
Range定期扫描清理:它遍历期间无法阻塞写入,可能漏删或重复删,且性能随数据量线性下降 - 过期判断必须用
time.Now()而非缓存时间戳,防止系统时间回拨导致误判
一个最小可行的带过期的 Get/Set 封装
下面是一个轻量封装,没用额外 goroutine,靠读操作驱动惰性过期:
type ExpiringMap struct {
m sync.Map
}
func (e *ExpiringMap) Set(key string, value interface{}, ttl time.Duration) {
expireAt := time.Now().Add(ttl)
e.m.Store(key, struct {
value interface{}
expireAt time.Time
}{value, expireAt})
}
func (e *ExpiringMap) Get(key string) (interface{}, bool) {
if v, ok := e.m.Load(key); ok {
entry := v.(struct {
value interface{}
expireAt time.Time
})
if time.Now().Before(entry.expireAt) {
return entry.value, true
}
// 过期了,顺手清理(失败也没关系,下次读再试)
e.m.LoadAndDelete(key)
}
return nil, false
}
注意:Get 中没加锁,完全依赖 sync.Map 的原子操作;LoadAndDelete 成功说明这是首个发现过期的 reader,失败则说明已被其他 goroutine 清理过——两种情况都不影响正确性。
什么时候该换别的方案
如果缓存命中率低、写多读少,或者需要精确的过期回调(比如过期时发通知)、LRU 驱逐、或大量 key 集中到期(如秒杀场景的 token 批量失效),sync.Map 就不太合适。此时应考虑 gocache、groupcache 或自己基于 map + RWMutex + heap 实现带定时器的结构——但那是另一层复杂度了。
真正容易被忽略的是:过期时间精度。用 time.Now() 每次调用都有开销,高频读场景下可考虑传入一个共享的 time.Time 快照,但得确保调用方能接受最多 1ms 级别的误差。

















