waitgroup 必须每请求新建,禁止全局复用或传值;add 必须在 go 前且一一对应;闭包需显式传参防循环变量捕获;wg.wait() 须配合 context 实现超时控制。

WaitGroup 在 Gin handler 里不能复用、不能传值、不能在 goroutine 里 Add,否则必 panic 或卡死。
WaitGroup 必须每请求新建一个
HTTP 请求是并发到达的,多个请求共享同一个 wg 变量会引发计数器污染。比如长连接或中间件中缓存了 wg 实例,第二轮请求调用 wg.Add(1) 时,计数可能已是负值或远大于预期。
- 错误写法:
var wg sync.WaitGroup声明在 handler 外(如全局变量、结构体字段) - 正确写法:每个 handler 函数内部第一行就声明
var wg sync.WaitGroup - 若需封装逻辑到子函数,必须把
&wg作为*sync.WaitGroup参数传入,不能传值
Add 必须在 go 语句之前,且一一对应
wg.Add(1) 和 go 是强顺序依赖关系。一旦颠倒,或漏掉某次 Add,wg.Wait() 就会永久阻塞(计数不归零),或提前返回(计数为 0 后又 Done → panic)。
- 禁止:
go func() { wg.Add(1); defer wg.Done(); ... }() - 禁止在循环内只写一次
wg.Add(len(tasks))却启动多个 goroutine —— Add 的时机必须紧邻go - 推荐固定模板:
wg.Add(1); go func(...) { defer wg.Done(); ... }(arg)
闭包传参必须显式传递,避免循环变量捕获
for 循环中直接用 for _, url := range urls { go func() { http.Get(url) }() } 会导致所有 goroutine 共享最后一个 url 值,且 defer wg.Done() 可能被跳过(因变量未绑定)。
- 错误现象:部分请求没触发
Done(),wg.Wait()卡住 - 正确写法:把变量作为参数传入匿名函数,如
go func(u string) { defer wg.Done(); http.Get(u) }(url) - 也可用局部变量赋值:
u := url; go func() { defer wg.Done(); http.Get(u) }()
别指望 WaitGroup 自带超时,必须配合 context
wg.Wait() 是纯阻塞调用,无法中断。真实业务中,某个 HTTP 请求卡住,不能让整个 handler 无限等下去。
- 错误做法:对
wg.Wait()包一层select { case —— <code>Wait()不响应任何 channel - 正确组合:用
context.WithTimeout控制整体生命周期,goroutine 内部用ctx.Done()做取消,再用select配合http.Client的WithContext - WaitGroup 只负责“是否全部结束”,超时判断和中断逻辑必须由 context 承担
最易被忽略的一点:wg 是值类型,但内部有原子状态字段;任何看似无害的赋值(如 subWg := wg)、传参(func f(wg sync.WaitGroup))、或 map 存储,都会导致计数器分裂——两个 wg 实例各自维护自己的计数,Wait() 永远等不到另一个的 Done()。











