Go 1.25 给 sync.WaitGroup 增加了 Go 方法。常见的 goroutine 启动代码可以从手工 Add、go、defer Done,缩成一次调用:
var wg sync.WaitGroup
for _, job := range jobs {
job := job
wg.Go(func() {
process(job)
})
}
wg.Wait()
这个 API 首先修正的是启动顺序。过去有人把 wg.Add(1) 写进 goroutine:
go func() {
wg.Add(1)
defer wg.Done()
process(job)
}()
wg.Wait()
Wait 可能在 Add 执行前看到计数为零,提前返回。Go 1.25 的 go vet 新增 waitgroup 检查,会报告这类误放的 Add。WaitGroup.Go 把计数增加与 goroutine 创建放进同一个标准方法,降低了写错顺序的机会。
它没有提供错误传播。f func() 没有返回值,业务函数失败后仍要自己决定如何记录、汇总或取消其他任务。若一组任务要求“任意一个失败就停止”,errgroup.Group 或自建带 context 的协调层仍更合适。不能为了使用新 API,把错误吞进日志后继续执行。
第二个边界是 panic。WaitGroup.Go 的文档要求传入函数不能 panic。即使运行时能够执行计数清理,业务也不能把 panic 当普通错误流。需要隔离不可信插件或任务时,应在明确的边界恢复 panic,附带堆栈并转成系统能够处理的失败状态,而不是假定 WaitGroup 会替你定义语义。
第三个边界是并发数量。下面代码会为每个 job 立即创建 goroutine:
for _, job := range jobs {
job := job
wg.Go(func() { process(job) })
}
任务有几十个时很自然,输入可能达到几十万时,goroutine 栈、参数、下游连接和队列都会形成压力。WaitGroup 负责“等它们结束”,不负责限流。可以在外层使用带容量的 channel 作为信号量,或创建固定数量 worker 从任务 channel 读取。
limit := make(chan struct{}, 32)
for _, job := range jobs {
job := job
limit <- struct{}{}
wg.Go(func() {
defer func() { <-limit }()
process(job)
})
}
wg.Wait()
这段代码仍要考虑取消。如果调用方 context 已结束,向 limit 发送可能继续阻塞。生产代码可以用 select 同时等待令牌与 ctx.Done(),并让 process 接收同一个 context。并发控制、取消与等待是三件事,API 简化了一件,不会自动组合其余两件。
嵌套调用也是新方法的一个实用点。只要 WaitGroup 非空,正在执行的 Go 启动的函数可以继续调用同一 WaitGroup 的 Go。但这并不意味着任意生命周期都安全。第一次 Go 必须发生在空 WaitGroup 的 Wait 之前;一轮任务结束后复用 WaitGroup,也要等上一轮 Wait 返回再启动下一轮。
迁移不必全仓替换。先运行 Go 1.25 的 go vet ./...,修复错误的 Add 位置。对单纯启动且不需要返回错误的任务,改用 WaitGroup.Go 能减少样板代码。已有 errgroup、worker pool 或专门任务执行器的地方,保留原结构更清楚。API 新不等于抽象层级更高。
测试方面,别只断言最终结果数量。加入取消、任务阻塞、输入为空、并发上限和内部启动子任务的用例。配合 race detector 检查共享数据,而不是把 WaitGroup 当作互斥锁。它只协调任务完成,不保护 map、slice 或计数器。
我会采用这条迁移规则:纯 fire-and-wait 任务用 WaitGroup.Go;需要错误与联动取消时用 errgroup;需要资源上限时加显式并发控制;共享状态另设同步。两行样板消失是好事,但并发程序真正难的部分从来不是那两行。
还有一个容易被代码审查漏掉的点:循环变量捕获。较新的 Go 版本已经调整了常见 for 循环变量语义,但项目的 go.mod 版本、旧代码写法和自定义变量仍要看清。显式写 job := job 虽然有时不再必要,却能让读者马上知道闭包使用的是本轮任务。迁移时不要顺手删除所有这类赋值。
基准测试也要区分 API 开销和业务吞吐。WaitGroup.Go 的目标是减少错误与样板,不是承诺比手工写法更快。若基准出现明显差异,先检查任务粒度、逃逸和测试噪声。对大多数网络服务,数据库与网络等待远大于这几行协调代码。
最后检查可观测性。批量启动任务时给每轮操作一个 trace 或批次标识,记录启动数、完成数、失败数和取消数。Wait 返回只能证明计数归零,不能证明每个任务达成业务结果。把完成协调与结果统计分开,事故时才能回答究竟丢在哪一步。