Go 1.25 的 WaitGroup.Go 省了两行代码,也补不上这三个并发边界

Go 1.25 给 sync.WaitGroup 增加了 Go 方法。常见的 goroutine 启动代码可以从手工 Add、go、defer Done,缩成一次调用:

go 复制代码
var wg sync.WaitGroup
for _, job := range jobs {
    job := job
    wg.Go(func() {
        process(job)
    })
}
wg.Wait()

这个 API 首先修正的是启动顺序。过去有人把 wg.Add(1) 写进 goroutine:

go 复制代码
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:

go 复制代码
for _, job := range jobs {
    job := job
    wg.Go(func() { process(job) })
}

任务有几十个时很自然,输入可能达到几十万时,goroutine 栈、参数、下游连接和队列都会形成压力。WaitGroup 负责"等它们结束",不负责限流。可以在外层使用带容量的 channel 作为信号量,或创建固定数量 worker 从任务 channel 读取。

go 复制代码
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 返回只能证明计数归零,不能证明每个任务达成业务结果。把完成协调与结果统计分开,事故时才能回答究竟丢在哪一步。

相关推荐
金銀銅鐵1 小时前
[Java] 用 CLI 展示 EnumSet 增减元素时的内部变化(下)
java·后端·ai编程
陈随易1 小时前
副业搞钱点子之收入查看器
前端·后端
Profile排查笔记1 小时前
浏览器指纹逆向怎么分析:从网络请求追踪到 JavaScript 采集链
前端·人工智能·后端·自动化
lazy H2 小时前
【计算机网络】第 5 篇:TCP 和 UDP 有什么区别?从 Java 后端场景理解两种传输协议
经验分享·后端·网络协议·tcp/ip·计算机网络·1024程序员节
东宇科技2 小时前
crmeb开源版本安装步骤2026-10-20
后端·php·crmeb
l1t2 小时前
在windows中测试musql数据库
数据库·sqlite·go·1024程序员节
IT_Octopus2 小时前
Apollo 配置中心入门:一所“小学校“读懂 Portal、Meta Server、Config Service
java·后端·spring
用户360055579003 小时前
Kestrel_VLLM - 19 · KV 复用迁移到 GPU(上):per-state 镜像与同类静默分叉
go
CHHH_HHH3 小时前
【Linux网络篇】初识网络与Socket编程基础
linux·服务器·c语言·开发语言·网络·后端·1024程序员节
弦儿3 小时前
后端 Web 实战:员工管理(新增员工 + 事务管理 + 文件上传)
java·后端