sync.waitgroup 不能直接与闭包变量一起用,因为 for 循环复用迭代变量,导致所有 goroutine 捕获同一地址的最终值,引发处理错乱或 panic;正确做法是将当前迭代值显式传入匿名函数参数。

为什么 sync.WaitGroup 不能直接和闭包变量一起用?
因为 Go 的 for 循环复用迭代变量,如果你在循环里启动 goroutine 并捕获 i 或 v,所有 goroutine 实际上看到的是同一个内存地址的最终值。常见错误是:10 个 goroutine 全部处理最后一个元素,或 panic:index out of range。
正确做法是把当前迭代值显式传入匿名函数参数,而不是依赖外部变量:
for i, task := range tasks {
wg.Add(1)
go func(idx int, t string) {
defer wg.Done()
process(task) // 注意:这里用 t,不是 task
}(i, task) // 立即传入当前值
}
- 不写成
go func() { ... }(i, task)—— 这会变成立即执行普通函数,不是 goroutine - 不要省略参数名(如
func(i, t)),Go 要求显式声明形参才能绑定实参 -
defer wg.Done()必须在 goroutine 内部调用,且不能放在匿名函数外层
如何避免 WaitGroup.Add() 调用时机错乱?
WaitGroup.Add() 必须在 goroutine 启动前调用,且不能在 goroutine 内部调用(否则可能 Add 晚于 Done,触发 panic:panic: sync: negative WaitGroup counter)。
典型错误模式:
go func() {
wg.Add(1) // ❌ 危险!Add 和 Done 不在同一线程视角下同步
defer wg.Done()
...
}()
- 必须在
go语句之前调用wg.Add(1) - 如果任务数动态生成(比如从 channel 接收),需先计数再 Add,或改用
errgroup.Group - 多个并发组混用时,别共用同一个
sync.WaitGroup实例——每个组应独立实例
匿名函数捕获变量时,哪些值该传参、哪些可直接引用?
只安全捕获不可变或只读共享数据(如配置结构体指针、全局 logger),但绝不能捕获会被循环修改的局部变量(task, i, data[j] 等)。
示例中常见的混淆点:
// ✅ 安全:config 是只读指针,且生命周期覆盖所有 goroutine
go func(t string) {
process(t, config) // config 可直接用
}(task)
<p>// ❌ 危险:data 是切片,data[i] 可能随循环变化
for i := range data {
go func() {
_ = data[i] // i 已失效
}()
}</p>
- 切片、map、channel 本身可安全传递(它们是引用类型),但索引访问(
data[i])必须固化 - 如果要打印日志或调试,建议在 goroutine 入口打快照:
log.Printf("task %d start", idx) - 闭包里调用的方法接收者如果是值类型,注意是否意外复制了状态
当任务需要返回结果时,为什么不能靠闭包变量收集?
闭包变量在并发下非线程安全,多个 goroutine 同时写 results[i] = x 可能导致数据竞争(race condition),即使用了 sync.WaitGroup 也无法避免。
正确方式是配合 channel 或 mutex:
// 方式1:用带缓冲 channel 收集(推荐)
ch := make(chan Result, len(tasks))
for _, t := range tasks {
wg.Add(1)
go func(task string) {
defer wg.Done()
ch
- 不要用
results = append(results, ...)在 goroutine 里直接操作切片——没有锁保护 - 若必须用 slice + mutex,确保每次写都加锁,且
append后及时解锁 -
sync.WaitGroup只管等待完成,不管数据同步;它和 channel/mutex 是互补关系,不是替代关系
真正麻烦的从来不是等完,而是等完之后怎么拿到各自的结果还不打架——这点容易被忽略。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











