Golang学习-sync.WaitGroup 等待组
sync.WaitGroup 等待组一、WaitGroup 解决什么问题当我们启动了一组 goroutine 去干活主 goroutine 需要在所有子 goroutine 完成之后再继续执行。比如并发抓取 10 个网页全部抓完再汇总结果批量处理文件所有 worker 跑完再写入汇总报告启动多个微服务健康检查全通过才算就绪WaitGroup 就是为这类等待一组并发任务完成的诉求设计的。二、三个核心方法WaitGroup 的 API 极为简洁——只有 Add、Done、Wait 三个方法varwg sync.WaitGroup// Add(n): 计数器加 n表示我又派了 n 个任务wg.Add(3)fori:0;i3;i{gofunc(idint){deferwg.Done()// 等价于 Add(-1)表示我干完了// ... 干活 ...}(i)}wg.Wait()// 阻塞直到计数器归零Done 内部就是wg.Add(-1)二者语义完全等价。三、设计哲学谁派生谁 Add这是最容易踩坑的地方。Add 必须在启动 goroutine之前调用否则可能 Wait 先执行直接放行// ❌ 错误wg.Add 在 go 里面fori:0;i5;i{gofunc(){wg.Add(1)// 严重错误Wait 可能在这之前就返回了deferwg.Done()doWork()}()}wg.Wait()正确的做法是 Add 和 go 语句紧挨在一起// ✅ 正确fori:0;i5;i{wg.Add(1)gofunc(){deferwg.Done()doWork()}()}wg.Wait()四、WaitGroup 是值类型必须传指针WaitGroup 结构体内部维护了一个计数器不能用做值拷贝的参数传递——副本中的加减不会影响原值。// ❌ 值传递计数器的修改对原始 wg 不可见funcrun(wg sync.WaitGroup){deferwg.Done()}// ✅ 指针传递funcrun(wg*sync.WaitGroup){deferwg.Done()}Go 1.20 编译器会给出 copylock 警告但不如一开始就用对。五、WaitGroup 不能重用飞行中的实例WaitGroup 的内部状态有三个阶段0 → Add → Wait。Wait 返回后WaitGroup 自动回到 0 状态可以重用。但如果在 Wait 还没返回时又调用 Add这属于并发访问内部状态是数据竞争。// ❌ 数据竞争gofunc(){wg.Add(1)// 与下面的 wg.Wait 并发deferwg.Done()doWork()}()wg.Wait()// 此时可能还没有 Add 完成六、练习代码保存为waitgroup_demo.gopackagemainimport(fmtmath/randsynctime)// 模拟从多个数据源获取数据typeDataFetcherstruct{namestringlatency time.Duration// 模拟耗时}func(f DataFetcher)Fetch()string{time.Sleep(f.latency)returnfmt.Sprintf([%s 数据],f.name)}funcmain(){rand.Seed(time.Now().UnixNano())// 三个数据源各自耗时不同sources:[]DataFetcher{{name:数据库,latency:200*time.Millisecond},{name:Redis,latency:50*time.Millisecond},{name:远程API,latency:300*time.Millisecond},}// 结果收集results:make([]string,len(sources))varwg sync.WaitGroupfori,src:rangesources{wg.Add(1)// 在 go 之前 Addgofunc(idxint,fetcher DataFetcher){deferwg.Done()results[idx]fetcher.Fetch()}(i,src)// 注意把循环变量传给闭包}fmt.Println(等待所有数据源返回...)wg.Wait()fmt.Println(全部就绪\n)fori,r:rangeresults{fmt.Printf(数据源 %d (%s): %s\n,i,sources[i].name,r)}// —— 进阶带超时的 WaitGroup ——fmt.Println(\n——— 超时模式演示 ———)varwg2 sync.WaitGroup wg2.Add(1)gofunc(){deferwg2.Done()time.Sleep(2*time.Second)// 模拟很慢的任务fmt.Println(慢任务完成)}()// 用一个 channel 来通知WaitGroup 已经等完了done:make(chanstruct{})gofunc(){wg2.Wait()close(done)}()select{case-done:fmt.Println(所有任务在超时前完成)case-time.After(500*time.Millisecond):fmt.Println(超时不等了)}}运行结果等待所有数据源返回... 全部就绪 数据源 0 (数据库): [数据库 数据] 数据源 1 (Redis): [Redis 数据] 数据源 2 (远程API): [远程API 数据] ——— 超时模式演示 ——— 超时不等了七、WaitGroup vs Channel场景WaitGroupChannel等待所有 goroutine 结束✅ 天然适合需要逐个接收写起来繁琐需要收集返回值需额外变量/锁可以直接从 channel 读取结果限制并发数不适合用有缓冲 channel 做信号量取消/超时需配合 select见上文配合 select ctx.Done()八、关键要点要点说明Add 在前必须在 go 语句之前调用 Adddefer Done确保 goroutine panic 也能 Done传指针WaitGroup 是值类型传递需取地址Wait 可重入多个 goroutine 可以同时 Wait超时模式Wait 本身不超时配合 channel select 实现不能复制复制 WaitGroup 的值会破坏内部状态