SingleFlight 是 Go 官方扩展库 golang.org/x/sync/singleflight 提供的并发控制工具,用于合并同一时刻对相同 key 的重复请求,避免缓存击穿或下游服务过载;它通过 Group.Do 实现“首个调用执行、其余等待并共享结果”的协调机制,而非缓存或超时控制。

SingleFlight 是什么,它解决什么问题
Go 的 singleflight 不是语言内置语法,而是标准库 golang.org/x/sync/singleflight 提供的并发控制工具,专用于「合并同一时刻发起的重复请求」。典型场景:多个 goroutine 同时查同一个 key 的缓存未命中,触发下游 DB 或 RPC 调用——不加控制就会发 N 次相同请求;用 singleflight.Group.Do 就能只执行一次,其余等待并共享结果。
怎么写一个安全可用的 Do 调用
singleflight.Group.Do 看似简单,但参数传错、panic 处理缺失、返回值类型不匹配,都会导致调用方阻塞或 panic。关键点:
-
key必须是可比较类型(string最常用),且语义上代表“同一请求”——比如缓存 key、用户 ID、API 路径等,不能用含指针或 map 的结构体 -
fn函数必须返回(interface{}, error),哪怕你实际需要*User,也得包装成interface{}后显式断言,否则类型错误会在运行时 panic -
Do本身不捕获fn内部 panic,必须在fn里用recover处理,否则整个 group 会卡死(后续同 key 请求永远阻塞)
示例:
var g singleflight.Group
func loadUser(id string) (*User, error) {
v, err, _ := g.Do(id, func() (interface{}, error) {
defer func() {
if r := recover(); r != nil {
log.Printf("loadUser panic: %v", r)
}
}()
u, err := db.QueryUser(id) // 可能 panic 或 error
return u, err
})
if err != nil {
return nil, err
}
return v.(*User), nil // 必须断言,不能省
}
为什么不能直接把 context 透传进 Do 的 fn
singleflight.Group.Do 接口不接收 context.Context,这是设计使然:它的职责只是去重和共享结果,不负责超时或取消。如果你在 fn 里用了带 timeout 的 HTTP client 或 DB 查询,context 应该在 fn 内部创建或传入,而不是指望 Do 帮你传递。
立即学习“go语言免费学习笔记(深入)”;
- 错误做法:试图把外部
ctx作为fn参数闭包进去,再用它控制子调用——这没问题,但别误以为Do会响应 cancel - 正确做法:在
fn内部新建子 context(如context.WithTimeout(context.Background(), 5*time.Second)),或接收已封装好 context 的依赖(如带 timeout 的 client 实例) - 注意:
Do返回后,context 是否 cancel 不影响已启动的fn执行,也不会中断正在等待的其他调用者——它们仍会等到结果或超时返回
合并请求后,error 和 result 怎么分发给不同调用者
singleflight 对所有等待者返回完全相同的 (val interface{}, err error)。这意味着:如果第一次执行返回了 err != nil,所有等待者都会收到这个 error;如果成功,所有人拿到同一个 val 的副本(注意是浅拷贝,若 val 是指针或 map,需自行深拷贝避免数据竞争)。
- 常见陷阱:把
val当作可变对象直接修改,结果影响其他 goroutine —— 比如返回&User{}后,A 修改了User.Name,B 下次读到的就是改过的值 - 规避方式:在
fn中返回不可变结构,或返回前做深拷贝(如用json.Marshal/Unmarshal,或github.com/mohae/deepcopy) - 更轻量做法:约定返回值为只读结构体,业务层不修改,或用
sync.Pool复用对象实例
真正容易被忽略的,是 error 类型本身是否可比较——如果自定义 error 包含 map 或 slice 字段,它就无法被 singleflight 正确缓存(虽然不影响运行,但违背语义)。确保 error 是纯值类型或指针类型即可。


















