singleflight 用于合并相同 key 的并发请求,仅首个执行 fn,其余等待共享结果;key 需稳定唯一(如缓存 key),shared 返回值标识是否为执行者,不可替代锁或用于缓存命中路径。

singleflight 不是用来“防并发”的,它是用来合并相同 key 的并发请求——只让第一个执行,其余等待共享结果。如果你指望它像 sync.Mutex 那样串行化所有操作,那会踩坑。
singleflight.Group.Do 的真实行为
Do 方法签名是:func (g *Group) Do(key string, fn func() (interface{}, error)) (val interface{}, err error, shared bool)
-
key是字符串,必须能准确标识“同一个请求”。比如缓存 key、用户 ID、接口参数的稳定哈希(不能用时间戳或随机数) -
fn是实际要执行的逻辑,它只被一个 goroutine 调用,其他同 key 请求会阻塞在Do内部的WaitGroup上 - 返回的
shared是布尔值:true 表示当前调用触发了fn执行;false 表示复用了别人的结果
常见错误:
- 把不同语义的请求塞进同一个
key(比如"user:123"同时用于查基本信息和查订单),导致结果错乱 - 在
fn里没做错误处理,一旦返回err != nil,所有等待者都拿到这个错误,包括本该成功的请求
示例中容易忽略的一点:
val, err, shared := g.Do("user:123", func() (interface{}, error) {
u, err := db.Query("SELECT * FROM users WHERE id = ?", 123)
if err != nil {
return nil, err // ❌ 这个 err 会被所有并发请求收到
}
cache.Set("user:123", u, time.Minute)
return u, nil
})
为什么不能直接套在缓存读取逻辑外层
典型误用写法:
func GetUser(id string) (*User, error) {
// ❌ 错误:缓存未命中才该用 singleflight,否则每次请求都进 Do,白等
val, err, _ := g.Do(id, func() (interface{}, error) {
if u, ok := cache.Get(id); ok {
return u, nil // 缓存命中的结果也被当成“执行结果”共享,但 cache.Get 本身无锁,可能脏读
}
u, err := db.QueryUser(id)
if err == nil {
cache.Set(id, u, ttl)
}
return u, err
})
return val.(*User), err
}
正确姿势是:先查缓存,未命中再进 Do
- 避免把缓存命中的廉价操作也拖进同步区
- 防止多个 goroutine 同时写缓存(虽然
cache.Set通常自己有锁,但没必要多一层竞争)
DoChan 和超时控制是保命手段
Do 是阻塞式,一旦 fn 卡住(比如 DB 连接池耗尽、下游接口 hang 死),所有同 key 请求都会无限等待。
DoChan 返回 chan Result,配合 select + time.After 可实现超时:
ch := g.DoChan("user:123", func() (interface{}, error) {
return db.QueryUser("123")
})
select {
case r := <p>注意:<code>DoChan</code> 不会自动取消正在执行的 <code>fn</code>,只是让等待者提前退出。真正的取消需靠 <code>context</code> 透传到 <code>fn</code> 内部(比如 <code>db.QueryContext</code>)。</p><hr><h3>
<code>singleflight</code> 和 <code>sync.Once</code> 的本质区别</h3>
-
sync.Once是进程生命周期内只执行一次,适合初始化;singleflight.Group是按 key 动态管理多个独立的 once 实例,每个 key 有自己的生命周期 -
Once.Do不接受参数,无法区分不同请求;Group.Do必须传key,天然支持多路复用 -
Once没有等待机制设计,不适用于需要结果分发的场景
最易混淆的点:有人以为 “用 Once 包一层 db.Query 就能防击穿”,但那样所有请求共用一个 Once,key 失效后第一次查询永远只能服务一个请求,其余全失败——这根本不是击穿防护,是人为制造瓶颈。
singleflight 的核心价值不在“防”,而在“合”:合请求、合 IO、合结果。它不解决数据一致性,也不替代缓存策略,但能让缓存失效那一刻的系统表现更可控。真正难的是 key 设计和错误传播边界的把控。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











