singleflight是go的golang.org/x/sync/singleflight包提供的重复请求抑制机制,用于同一key的并发请求仅执行一次函数,其余等待共享结果;适用于缓存击穿、突发重复查询等场景,非缓存亦非限流器。
singleflight 是什么,什么时候该用它
singleflight 是 Go 标准库 golang.org/x/sync/singleflight 提供的并发控制工具,核心作用是:对相同 key 的并发请求,只让一个 goroutine 真正执行函数,其余等待并共享结果。它不是缓存,不持久化结果;也不是限流器,不控制 QPS。典型适用场景是「突发大量重复查询」——比如服务刚启动时几十个 goroutine 同时查同一个配置、或上游未做去重直接广播了相同 ID 的批量请求。
常见错误现象包括:数据库被打爆(同一 SQL 执行上百次)、下游 HTTP 接口被刷出 429、Redis 频繁穿透。这时候加一层 singleflight.Group.Do 往往比加缓存更快见效,尤其当原始操作本身很快(
怎么用 Do 方法合并请求
Do 是最常用入口,签名是 func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool)。关键点:
-
key必须能准确标识“相同请求”,比如把user_id=123和region=us拼成"user:123:us",而不是只用123 -
fn内部必须是无副作用的操作(不能改全局状态、不能发异步消息),因为可能被多次调用(失败重试时)或只被调用一次(成功时) - 返回的
shared是布尔值:true 表示当前返回的是别人执行的结果,false 表示自己执行的
var g singleflight.Group
<p>func getUser(id int) (<em>User, error) {
v, err, shared := g.Do(fmt.Sprintf("user:%d", id), func() (interface{}, error) {
u, err := db.QueryUser(id) // 真实 DB 查询
return u, err
})
if err != nil {
return nil, err
}
return v.(</em>User), nil
}</p>
注意:类型断言前最好加判断,避免 panic;如果 fn 可能 panic,Do 会把它转为 error,但不会传播 panic。
DoChan 和 Forget 的使用边界
DoChan 返回 chan Result,适合不想阻塞当前 goroutine 的场景,比如在 HTTP handler 中启动后立即返回 pending 状态,再用 select 处理超时或结果。但它不自动清理 channel,如果接收方没读完,channel 会一直占内存。
Forget 用于主动清理某个 key 的 pending 状态,典型用途是:你发现某个 key 对应的请求已永久失效(比如用户被删除),后续对该 key 的 Do 应该跳过 group 直接执行新逻辑。但要注意:Forget 不会中断正在运行的 fn,只影响新进来的请求。
- 不要频繁调用
Forget,它内部有锁,高并发下调用反而成瓶颈 -
DoChan的 channel 容量为 1,多次调用DoChan同一个 key,只有第一个能写入,其余会阻塞直到 channel 被读走 - 如果业务需要“取消进行中的请求”,
singleflight不支持,得换context.Context+ 自研机制
容易被忽略的坑:panic、error 类型、GC 压力
singleflight 的 group 内部用 map 存 pending 请求,key 是字符串,value 是带 channel 的结构体。如果 key 无限增长(比如拼入时间戳、随机数),map 会持续膨胀,引发内存泄漏。必须确保 key 具备收敛性。
fn 返回的 error 会被原样透传,但如果 fn panic,Do 会捕获并返回 fmt.Errorf("panic: %v", recover()) —— 这意味着你无法区分“业务错误”和“panic 错误”,日志里也看不到 panic stack。建议在 fn 内部用 defer/recover 显式处理异常。
返回值如果是大对象(比如 1MB 的 JSON 字节流),所有等待者共享同一份内存,看似节省,但若调用方做了深拷贝或长期持有,GC 无法回收,可能拖慢整个程序。这时不如在 fn 内部做一次浅拷贝再返回。
key 的构造尽量避开非必要字段,比如不要把整个 request body 当 key;HTTP 请求中,优先用路径参数和 query 参数的确定子集,而非 raw URL。











