
本文介绍使用 sync.waitgroup 实现对未知数量任务的并发处理与同步等待,避免竞态和提前退出,确保所有 goroutine 完成后才继续主流程。
本文介绍使用 sync.waitgroup 实现对未知数量任务的并发处理与同步等待,避免竞态和提前退出,确保所有 goroutine 完成后才继续主流程。
在 Go 中,当需要并发处理一批动态生成或来源未知(如 channel 流、数据库查询结果、文件行、递归遍历节点等)的任务时,常见的误区是依赖 time.Sleep 或手动计数 channel 关闭来“猜测”任务何时结束。这既不健壮也不符合 Go 的并发哲学。正确的做法是使用 sync.WaitGroup —— 它专为这类“等待一组 goroutine 完成”场景设计。
✅ 正确模式:WaitGroup + goroutine 分发
核心逻辑三步走:
- 初始化 WaitGroup;
-
在启动每个 goroutine 前调用
wg.Add(1)(注意:必须在 goroutine 启动前,且不能并发调用 Add); -
每个 goroutine 结束时调用
wg.Done()(通常用defer wg.Done()保证执行); -
主 goroutine 调用
wg.Wait()阻塞等待全部完成。
以下是一个完整、可运行的示例,模拟从无界 channel 中读取未知数量的整数并并发处理:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
// 模拟动态产生数据的 source(例如:API 分页响应、DFS 遍历节点等)
source := make(chan int, 10)
go func() {
defer close(source)
for i := 1; i <= 7; i++ { // 实际中数量未知
source <- i
}
}()
var wg sync.WaitGroup
results := make([]int, 0, 10) // 若需收集结果,建议配合 mutex 或带缓冲 channel
var mu sync.Mutex
// 并发消费 source 中的所有项
for item := range source {
wg.Add(1)
go func(val int) {
defer wg.Done()
// 模拟耗时处理(如 HTTP 请求、正则匹配、解析等)
time.Sleep(100 * time.Millisecond)
fmt.Printf("processed: %d\n", val)
// 安全写入共享结果切片(需加锁)
mu.Lock()
results = append(results, val*2)
mu.Unlock()
}(item)
}
// 等待所有 goroutine 完成
wg.Wait()
fmt.Println("All done. Results:", results)
}⚠️ 关键注意事项
-
wg.Add()必须在go语句之前调用:否则可能因 goroutine 过早执行wg.Done()导致 WaitGroup 计数器归零,wg.Wait()提前返回。 -
避免在循环内直接传入循环变量:如
go process(i, wg)中的i是共享变量,应显式捕获go func(val int) { ... }(i),防止闭包引用错误。 - 不要混用 channel 关闭与 WaitGroup:channel 用于数据传递,WaitGroup 用于生命周期同步——二者职责分离,不可互相替代。
-
若需收集结果,优先考虑带缓冲 channel 而非共享内存:例如
resultCh := make(chan Result, cap),每个 goroutine 发送resultCh ,主 goroutine 在 <code>wg.Wait()后关闭 channel 并 range 接收,更符合 Go 的 CSP 思想。
✅ 总结
对于“未知数量任务的并发执行与等待”,sync.WaitGroup 是标准、轻量、线程安全的解决方案。它不依赖超时、不假设数据源长度、不引入竞态风险,是 Go 并发编程中的基础支柱之一。掌握其正确用法,是写出可靠高并发 Go 服务的关键一步。

















