登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

WaitGroup Go 方法调用顺序的并发收尾

来源:17golang原创

时间:2026-10-11 01:17:28 455浏览 收藏

使用 sync.WaitGroup 做并发收尾时,最稳的顺序是:先在计数还是空的时候启动任务,再等待计数归零。Go 1.25 起可以用 WaitGroup.Go 把“增加计数、启动 goroutine、任务结束后减少计数”收进一个调用;如果继续使用传统写法,则必须把正数 Add 放在创建 goroutine 之前。

官方文档:https://pkg.go.dev/sync#WaitGroup

一句话记忆:空的 WaitGroup 上,Go 或正数 Add 要先于 Wait;一轮任务结束后,下一轮才可以重新添加任务。

先确定任务启动与 Wait 的边界

可以把并发收尾拆成三个边界:主 goroutine 负责建立任务计数,任务函数负责在正常返回时完成收尾,Wait 只负责等待计数归零。这样看,Wait 不是“启动任务”的方法,也不会替调用方补上尚未建立的计数。

WaitGroup、WaitGroup.Go、任务计数与 Done 的静态启动收尾关系说明图
图1:WaitGroup 启动与收尾边界说明图,展示 Go、任务计数、Done 和 Wait 的职责关系;这是结构说明图,不是运行截图。

因此,下面的最小形式是清楚的:

package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup

	// Go 负责增加任务计数并启动函数;空计数时它必须先于 Wait。
	wg.Go(func() {
		// 任务正常返回后,WaitGroup 会自动完成收尾。
		fmt.Println("worker finished")
	})

	// Wait 只等待计数归零,不负责创建任务。
	wg.Wait()
	fmt.Println("all tasks finished")
}

WaitGroup.Go 的函数不能 panic;如果任务可能把错误作为结果返回,应把错误写入受控的结果通道或错误收集器,而不是依赖 panic 传播。任务内部还可以继续调用同一个 WaitGroup 的 Go,但空计数时仍然要确保外层先建立任务,再进入 Wait。

传统 Add/Done 写法要把计数放在 goroutine 外

兼容旧版本 Go 或需要显式控制任务数量时,可以使用 Add、Done 和 Wait。真正关键的不是方法名称,而是正数 Add 和 Wait 的先后关系:当计数为零时,正数 Add 必须发生在 Wait 之前,通常也要发生在创建 goroutine 之前。

package main

import (
	"fmt"
	"sync"
)

func runBatch(items []string) {
	var wg sync.WaitGroup
	wg.Add(len(items)) // 先建立完整计数,避免 Wait 看到空计数。

	for _, item := range items {
		item := item // 为每个 goroutine 固定本轮循环值。
		go func() {
			defer wg.Done() // 无论函数从哪个正常路径返回,都减少一次计数。
			fmt.Println("processing", item)
		}()
	}

	wg.Wait() // 只有计数归零后才继续收尾。
}

把 wg.Add(1) 写进新 goroutine 是常见错误:主 goroutine 可能先执行 Wait,此时计数仍为零,Wait 会直接返回,任务计数就和实际任务脱节。需要动态增加任务时,也要保证增加发生在当前 WaitGroup 已经有任务、或者上一轮 Wait 已经返回的安全边界内。

复用 WaitGroup 时不要跨轮添加任务

WaitGroup 可以用于多轮独立任务,但上一轮的 Wait 尚未返回时,不要开始下一轮的正数 Add。将它理解为一张一次只服务一个任务集合的收尾账单即可:先记账,再消费,归零并收口后才能开下一张账。

WaitGroup Add、Done、Wait 与分轮复用边界的静态关系说明图
图2:WaitGroup 分轮复用契约说明图,展示上一轮 Wait 返回后才能进入下一轮 Add/Go;这是结构说明图,不是运行截图。
func processRounds(rounds [][]string) {
	var wg sync.WaitGroup

	for _, round := range rounds {
		wg.Add(len(round)) // 本轮开始前登记本轮任务。
		for _, name := range round {
			name := name
			go func() {
				defer wg.Done() // 当前任务完成后归还一个计数。
				process(name)
			}()
		}
		wg.Wait() // 本轮完全收口后,才进入下一轮。
	}
}

如果不需要按轮等待,而是想让任务持续追加,可以让 WaitGroup 在计数大于零时继续接收新的 Go;但最终调用 Wait 的位置仍要由拥有任务集合的那一层决定。不要复制已经使用过的 WaitGroup,也不要把它当作可传递的业务结果容器。

并发收尾的四个落地检查点

检查项正确边界常见后果
空计数启动Go 或正数 Add 先于 WaitWait 提前返回
任务收尾传统写法在 goroutine 内用 defer Done计数无法归零或出现负数 panic
分轮复用上一轮 Wait 返回后再进入下一轮 Add/Go任务集合边界不清
对象所有权WaitGroup 首次使用后不复制并发状态被拆成两份

最后再补一个版本选择:Go 1.25 及以上、任务函数不会 panic 时,WaitGroup.Go 更容易把顺序写对;需要兼容旧版本或显式登记一批任务时,Add/Done/Wait 仍然合适。无论采用哪种接口,都应把“谁负责启动、谁负责收尾、哪一层负责等待”写在同一个局部结构里。

常见问题

为什么不能在 goroutine 内先 Add 再 Wait?

因为新 goroutine 何时真正运行没有保证,主 goroutine 可能先执行 Wait。如果此刻计数还是零,Wait 会直接返回。

WaitGroup.Go 能不能替代错误处理?

不能。它只管理任务计数和等待关系,不会替你聚合错误;错误应通过结果通道、受控的共享结构或其他明确的取消/收集方案传回调用方。

把方法顺序固定下来后,WaitGroup 的排查就不再是“偶发并发问题”:先看空计数时是否先启动,接着看每个任务是否完成收尾,最后看下一轮是否等到了上一轮的 Wait 返回。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>