singleflight 不能代替缓存,仅实现请求去重:同一时刻相同 key 下仅一个 goroutine 执行函数,其余等待共享结果;需配合本地缓存(如 sync.map)使用,且 key 必须稳定可复现、group 全局复用、fn 内含完整加载逻辑与超时控制。

singleflight 不能代替缓存,只做请求去重
它不存数据,也不管你查没查过——Do 只保证「同一时刻、相同 key」的调用里,只有一个 goroutine 真正执行函数,其余全等着分结果。所以如果你没配本地缓存(比如 sync.Map),那每次缓存 miss 后走一次 Do,下个请求来还是 miss,照样再进一次 Do,数据库照打不误。
- 必须先查本地缓存 → miss 后才进
sfGroup.Do(key, fn) -
fn里要包含完整加载逻辑(DB 查询 + 写缓存),不能把cache.Store()拿到外面写 - 别用
sync.Once替代——它不认key,对不同 ID 完全无效
key 设计错,singleflight 就等于没开
HTTP 请求里带 X-Request-ID、时间戳、随机参数?或者直接用 userID int 值传进 Do?这些都会让本该合并的请求散成几十个 key,sfGroup.Do 形同虚设。压测时 DB 查询数随并发线性上涨,就是这个原因。
- key 必须稳定可复现:推荐
fmt.Sprintf("user:%d", id)或strconv.Itoa(id) - 排除非幂等字段:过滤掉 header 中的 trace ID、query 中的
ts=等 - 别把 struct 直接当 key 传——
Do要求 string,传错会 panic
Group 生命周期写错,每次请求都新建一个
把 var g singleflight.Group 写在 handler 函数里,或者每次 new 一个新 Group,那就彻底失去去重能力。因为每个 Group 内部维护独立的 map[string]*call,互相看不见对方在忙啥。
- 全局声明、复用一个
singleflight.Group实例(它是线程安全的) - 不要在中间件里无差别 wrap 所有路由——健康检查、静态资源、OPTIONS 预检请求不该进
Do - 如果服务长期运行,无需担心 Group 泄漏;但若需清理某 key 的等待状态,可用
Forget(key)
错误处理和超时控制常被忽略
Do 返回的 err 是底层 fn 抛出的错误,所有等待者共享同一个 error。如果 fn 里没加 context 控制,一个慢查询卡住 5 秒,后面所有 goroutine 都得干等——不是超时失败,是真·卡死。
- 务必在
fn内使用带 timeout 的 context,比如ctx, cancel := context.WithTimeout(ctx, 200*time.Millisecond) - 错误发生时,别往缓存里写空值或默认值——除非业务明确要求缓存空对象防穿透
-
Do第二个参数必须是func() (interface{}, error),少一个返回值或类型不对,编译不过
最麻烦的不是不会写,而是你以为写了就生效了——key 不稳、Group 错位、缓存写漏、超时不设,四个点只要中一个,singleflight 就退化成装饰器。上线前最好用两个并发 goroutine 打同一个 key,抓日志看 DB 查询是否真只出现一次。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











