singleflight 是 Go 的请求去重器,用于缓存击穿时将 N 次相同查询合并为 1 次执行,适用于读多写少、查询开销大、结果可共享的场景,如查库或调远程 API。

singleflight 是什么,什么时候该用它
它不是缓存,也不是锁,而是一个请求去重器:当多个 goroutine 同时调用 Do 执行同一个 key 的函数时,只让其中一个真正执行,其余阻塞等待结果返回。适合「读多写少 + 查询开销大 + 结果可共享」的场景,比如查数据库、调远程 API、解析大 JSON。
常见错误现象:cache miss 后大量并发请求同时穿透到 DB,CPU 和连接数陡增;或者你写了 sync.Once 但发现它只能用一次,没法按 key 区分。
注意:它不解决缓存过期问题,也不替代 Redis。它只是在「缓存没命中、正要查库」那一瞬间,把 N 次查询压成 1 次。
怎么用 singleflight.Do 防止重复查库
核心是把「查库逻辑」包进 Do 的回调里,key 要能唯一标识这次查询(比如 "user:123"),别用时间戳或随机数。
立即学习“go语言免费学习笔记(深入)”;
- 必须传入非空
key,否则 panic;key 类型任意,但建议用string或可比较类型 -
Do返回(interface{}, error, bool):第三个bool表示是否是发起者(true表示真去执行了,false表示等别人的结果) - 回调函数不能 panic,否则所有等待者都会收到那个 panic 转成的
error - 示例:
var g singleflight.Group result, err, shared := g.Do("user:123", func() (interface{}, error) { return db.QueryRow("SELECT name FROM users WHERE id = ?", 123).Scan(&name) })
为什么不能直接用 sync.Map 或 channel 替代
sync.Map 只做键值存储,不协调执行;channel 要自己管理缓冲、关闭、超时,容易写出死锁或漏 recover 的代码。而 singleflight.Group 内部用 map + sync.Mutex + sync.WaitGroup 封装好了这些细节,且支持取消(配合 context)。
性能影响很小:单个 Group 在高并发下热点 key 会竞争 mutex,但通常远小于 DB 查询开销;如果真有极端吞吐需求,可以按业务维度拆多个 Group 实例(比如 userGroup、orderGroup)。
兼容性没问题:singleflight 是 Go 标准库 golang.org/x/sync/singleflight,Go 1.9+ 均可用,无需额外依赖。
容易被忽略的坑:超时、错误传播和 key 泄漏
最常踩的三个点:
-
Do不接受context,超时得靠回调函数自己控制 —— 别在回调里写无限制的time.Sleep或死循环 - 如果回调返回
error,所有等待者都拿到同一个 error,且不会重试;想区分「临时失败」和「永久失败」,得在回调里做重试逻辑或包装 error 类型 - key 如果是拼接的字符串(比如
"user:" + strconv.Itoa(id)),要注意id为负数或空时生成非法 key,导致去重失效或 panic - Group 实例生命周期要管好:长期运行的服务里,不要每次 HTTP 请求都 new 一个
Group,它内部 map 不清理,key 多了会内存泄漏
key 泄漏比想象中更隐蔽 —— 比如用户 ID 来自 URL 参数,没校验就直接拼进 key,攻击者刷一堆非法 ID,就能让 map 不断膨胀。










