sync.RWMutex 是唯一能守住并发缓存清空安全边界的锁机制,因其支持读写分离,可确保多goroutine读不阻塞、写操作独占,避免“concurrent map iteration and map write”崩溃;其他方式如直接遍历删除、sync.Map或无锁清理均无法满足安全要求。

sync.RWMutex 是唯一能守住并发缓存清空安全边界的锁机制,不用它,panic: concurrent map iteration and map write 不是概率事件,而是必然崩溃。
为什么不能直接遍历 delete(map, key)
Go 的原生 map 不支持并发读写。只要有一个 goroutine 在执行 cache[key](读),另一个在执行 delete(cache, key) 或遍历后删(写),运行时立刻 panic。这不是偶发 bug,是语言层强制约束。
- 错误现象:
fatal error: concurrent map iteration and map write,堆栈里常出现runtime.mapiternext和runtime.mapdelete - 常见误用:在 HTTP handler 里调用清理函数,同时有其他 goroutine 正在
Get缓存项 - 陷阱:用
sync.Map也不能解决“遍历 + 删除”问题——它不提供安全遍历接口,LoadAll()需自己实现且仍要加锁 - 正确路径:所有读操作走
RWMutex.RLock()/RUnlock();所有清理逻辑必须先Lock(),全程独占,且清理回调里不能再触发任何缓存读写
清理前必须显式释放资源,不能只 delete
缓存 value 常含 *os.File、net.Conn、[]byte 大缓冲或引用计数对象。单纯 delete(cache, key) 只断开 map 引用,底层资源不会自动释放,造成 fd 泄漏或内存持续增长。
- 定义统一清理契约:
type Cleaner interface{ Cleanup() } - 在清理函数中检查并调用:
if c, ok := val.(Cleaner); ok { defer func() { recover() }(); c.Cleanup() } - 禁止在
Cleanup()内部再调用cache.Get()或cache.Delete(),否则可能死锁或重入 - 若 value 是结构体字段含指针或切片,需手动置
nil,比如v.buf = nil,避免 GC 无法回收底层数组
按条件清理比全量清空更稳、更快
生产环境极少需要 DeleteAll()。全量遍历 map 并逐个 delete 会持写锁数百毫秒,阻塞所有读请求,等同于 STW。更危险的是,它掩盖了真实淘汰策略缺失的问题。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 推荐模式:为每个 entry 加
expiresAt time.Time字段,在Get()时惰性淘汰(if time.Now().After(e.expiresAt)) - 定时清理:每分钟触发一次,但单次最多删 200 条,删完立即
Unlock(),避免长锁 - 前缀清理:用
strings.HasPrefix(k, "session:")筛选,比全量快一个数量级 - 避免
for k := range cache { delete(cache, k) }—— 这种写法在锁内执行,且 range 本身在迭代过程中被 delete 修改,行为未定义
测试必须跑 -race,且覆盖并发读写混合场景
单元测试里只跑 DeleteAll() 单 goroutine 用例毫无意义。真正要验证的是:当 10 个 goroutine 持续 Get,1 个 goroutine 调用清理时,是否 panic 或数据错乱。
立即学习“go语言免费学习笔记(深入)”;
- 写测试时用
go func() { for i := 0; i 启多个读协程 - 主 goroutine 间隔 1ms 触发一次清理,持续 100ms
- 必须用
go test -race运行,否则竞争条件根本暴露不出 - 清理函数内部的
Cleanup()调用要用defer func(){recover()}()包裹,防止个别实现 panic 导致整个清理中断

















