Forget不是缓存失效机制,仅从group.m中删除pending请求记录,不影响外部缓存;它不清理Redis或sync.Map,也不触发重载,只改变singleflight自身并发协调状态。

singleflight.Group.Forget 是什么,它能主动失效缓存吗?
singleflight.Group.Forget 不是缓存失效机制,它只是从内部的 group.m(一个 map[string]*call)中删除指定 key 对应的 pending 请求记录。它不操作任何外部缓存(比如 Redis、内存 map),也不触发重载或清理业务层数据。如果你期望调用 Forget 后下次 Do 就“重新查库”,那确实会如愿——但前提是:你没在 Do 的 fn 里自己做缓存读写;否则,Forget 对你的 cache.Map 或 redis.Get 完全无感。
- 它只影响 singleflight 自身的“正在飞行中”状态,不影响业务缓存生命周期
- 它不能替代
cache.Delete或redis.Del - 它在 key 无 pending 请求时静默返回,不报错
什么时候该调用 Forget?典型场景和正确时机
最常见且合理的使用场景是:你明确知道某个 key 对应的数据已过期/被修改,希望中断所有正在等待该 key 结果的 goroutine,并让下一次请求真正穿透到底层去加载新值。
例如:用户资料更新后,你想让后续对该用户的 GetUserProfile(id) 立即走新逻辑,而不是等完上一个还在跑的 slow DB 查询。
- 必须在数据变更之后、且确定旧 pending 请求已无意义时调用,比如 DB 更新成功后立即
g.Forget(key) - 不要在
Do的 fn 内部调用Forget—— 此时 key 还在 map 中,强行删会导致 panic(因为call正在执行中,而Forget删除后call.wg.Done()可能写已释放内存) - 如果用的是带 TTL 的缓存(如
fastcache),Forget和缓存Delete应配合使用,二者职责分离:前者管并发协调,后者管数据新鲜度
Forget 调用后 Do 行为变化:为什么有时没效果?
调用 Forget 后,下一次 Do(key, fn) 会像首次调用一样新建 call 并执行 fn,但以下情况会让“失效”看起来没发生:
-
fn内部仍先查本地内存缓存(如sync.Map),而你没清那个缓存 → 实际没穿透 - key 拼写不一致(比如一次用
"user:123",一次用"u123"),Forget对错 key 无效 - 多个
singleflight.Group实例混用,你在 A 组Forget,却在 B 组Do - 使用了
DoChan或DoContext,但没检查返回的error是否为singleflight.ErrCanceled,导致误以为请求成功(其实被Forget中断了)
示例片段:
// 正确配对:改 DB + 清缓存 + Forget
db.UpdateUser(id, newProfile)
redis.Del(ctx, "user:"+id)
userGroup.Forget("user:"+id) // 让下次 Do 重建 call
<p>// 错误:Forget 放在 Do 的 fn 里
userGroup.Do("user:"+id, func() (interface{}, error) {
userGroup.Forget("user:"+id) // ⚠️ panic 风险,且无意义
return loadFromDB(id)
})
Forget 在 Go 1.21+ 的行为差异与兼容性注意
Go 1.21 起,singleflight.Group 内部加了 mu 读写锁保护 m,Forget 已线程安全;但老版本(<1.20)中若并发调用 Forget 和 Do,可能触发 map 并发读写 panic。
- Go ≤ 1.19:必须确保
Forget不与Do对同一 key 并发执行,建议加额外锁或用 channel 序列化 - Go ≥ 1.20:
Forget安全,但仍需避免在fn执行中调用(文档明确禁止) - 所有版本都不支持“批量 Forget”,只能逐 key 调用
真正容易被忽略的一点:Forget 不会唤醒或取消已阻塞在 Do 上的 goroutine,它只是删掉 key 对应的 *call。那些已经进入 call.wg.Wait() 的协程,会继续等原 call 完成(哪怕你刚删了它)——因为删除动作不修改已有 call 的状态。所以 Forget 的实际作用窗口,仅限于“尚未开始 Wait,但已进入 group.m 查找”的请求。


















