Go等待協(xié)程之WaitGroup使用與避坑指南
引言
在 Go 并發(fā)編程中,goroutine 非常輕量,但如何優(yōu)雅地等待多個(gè)協(xié)程執(zhí)行完成,才是工程實(shí)踐中的關(guān)鍵問(wèn)題。
很多人第一反應(yīng)是用 channel,但在“只關(guān)心完成,不關(guān)心結(jié)果”的場(chǎng)景中,sync.WaitGroup 才是更合適的工具。
這篇文章不僅講用法,還會(huì)帶你深入理解 WaitGroup 的本質(zhì)、實(shí)現(xiàn)機(jī)制,以及那些非常容易踩的坑。
什么是 WaitGroup
WaitGroup 本質(zhì)上是一個(gè)協(xié)程計(jì)數(shù)器 + 阻塞等待機(jī)制。
它解決的問(wèn)題是:
主協(xié)程如何等待一組子協(xié)程執(zhí)行完成?
來(lái)看一個(gè)最簡(jiǎn)單的模型:
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(3) // 設(shè)置需要等待的 goroutine 數(shù)量
go func() {
defer wg.Done() // 執(zhí)行完任務(wù)后,計(jì)數(shù)器減1
fmt.Println("任務(wù)1完成")
}()
go func() {
defer wg.Done() // 執(zhí)行完任務(wù)后,計(jì)數(shù)器減1
fmt.Println("任務(wù)2完成")
}()
go func() {
defer wg.Done() // 執(zhí)行完任務(wù)后,計(jì)數(shù)器減1
fmt.Println("任務(wù)3完成")
}()
wg.Wait() // 阻塞,直到計(jì)數(shù)器歸零
fmt.Println("所有任務(wù)完成")
}
輸出:
任務(wù)3完成 任務(wù)1完成 任務(wù)2完成 所有任務(wù)完成
小結(jié)
Add(n):設(shè)置任務(wù)數(shù)量Done():任務(wù)完成(等價(jià)于Add(-1))Wait():阻塞直到計(jì)數(shù)器為 0
核心思想:計(jì)數(shù)器歸零 → 主協(xié)程繼續(xù)執(zhí)行
使用示例(逐步深入)
基礎(chǔ)示例:等待多個(gè)任務(wù)
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1) // 增加計(jì)數(shù)器
// 啟動(dòng)goroutine處理任務(wù)
go func(i int) {
// 任務(wù)完成后,計(jì)數(shù)器減1
defer wg.Done()
fmt.Println("處理任務(wù):", i)
}(i) // 注意這里的i是值傳遞,而不是引用傳遞
}
wg.Wait()
fmt.Println("全部完成")
}
輸出:
處理任務(wù): 2 處理任務(wù): 0 處理任務(wù): 1 全部完成
示例進(jìn)階:結(jié)合業(yè)務(wù)處理
package main
import (
"fmt"
"sync"
"time"
)
// 并發(fā)執(zhí)行
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // 告訴主協(xié)程,子協(xié)程已經(jīng)執(zhí)行完畢
fmt.Println("worker", id, "開(kāi)始")
time.Sleep(time.Second)
fmt.Println("worker", id, "結(jié)束")
}
func main() {
// 并發(fā)執(zhí)行多個(gè) worker
var wg sync.WaitGroup
// 等待5個(gè)協(xié)程執(zhí)行完畢
for i := 0; i < 5; i++ {
// 告訴主協(xié)程,子協(xié)程還沒(méi)執(zhí)行完畢
wg.Add(1)
// 并發(fā)執(zhí)行子協(xié)程
go worker(i, &wg)
}
// 等待所有協(xié)程執(zhí)行完畢
wg.Wait()
fmt.Println("所有 worker 執(zhí)行完畢")
}
輸出:
worker 4 開(kāi)始 worker 0 開(kāi)始 worker 1 開(kāi)始 worker 2 開(kāi)始 worker 3 開(kāi)始 worker 3 結(jié)束 worker 4 結(jié)束 worker 0 結(jié)束 worker 1 結(jié)束 worker 2 結(jié)束 所有 worker 執(zhí)行完畢
示例進(jìn)階:錯(cuò)誤處理(常見(jiàn)誤區(qū)前奏)
WaitGroup 不能直接獲取返回值,如果你需要結(jié)果,必須結(jié)合 channel:
package main
import (
"fmt"
"sync"
)
// 定義一個(gè) worker,將計(jì)算結(jié)果發(fā)送到 channel 中
func worker(id int, wg *sync.WaitGroup, ch chan<- int) {
// 告訴 WaitGroup 我們已經(jīng)完成了
fmt.Println("worker", id, "starting")
defer wg.Done()
ch <- id * 2
fmt.Println("worker", id, "done")
}
func main() {
var wg sync.WaitGroup
// 創(chuàng)建一個(gè) channel,大小為5
ch := make(chan int, 5)
for i := 1; i < 5; i++ {
wg.Add(1)
go worker(i, &wg, ch)
}
// 等待所有 worker 都完成
wg.Wait()
// 關(guān)閉 channel,防止阻塞
close(ch)
// 從 channel 中讀取數(shù)據(jù)
for v := range ch {
// v 就是從 channel 中接收到的值
fmt.Println("結(jié)果:", v)
}
}
輸出:
worker 4 starting worker 4 done worker 1 starting worker 1 done worker 2 starting worker 2 done worker 3 starting worker 3 done 結(jié)果: 8 結(jié)果: 2 結(jié)果: 4 結(jié)果: 6
小結(jié):
- WaitGroup 解決“同步問(wèn)題”(等完成)
- channel 解決“通信問(wèn)題”(傳結(jié)果)
- 兩者通常組合使用,而不是互相替代
思考點(diǎn)
為什么 WaitGroup 不設(shè)計(jì)成可以直接返回結(jié)果?
因?yàn)樗穆氊?zé)非常單一:只做“等待”這件事,避免職責(zé)膨脹。
常見(jiàn)坑(重點(diǎn))
這里是實(shí)際開(kāi)發(fā)中最容易翻車(chē)的地方。
坑一:Add 寫(xiě)在 goroutine 里(致命問(wèn)題)
錯(cuò)誤寫(xiě)法:
for i := 0; i < 3; i++ {
go func() {
wg.Add(1) // ? 錯(cuò)誤
defer wg.Done()
fmt.Println("任務(wù)")
}()
}
wg.Wait()
問(wèn)題:
Wait()可能先執(zhí)行Add()還沒(méi)來(lái)得及執(zhí)行- 直接 panic:
sync: WaitGroup misuse
正確寫(xiě)法:
wg.Add(1)
go func() {
defer wg.Done()
}()
小結(jié)
Add 必須在啟動(dòng) goroutine 之前執(zhí)行
坑二:多調(diào)用 Done 導(dǎo)致負(fù)數(shù) panic
wg.Add(1)
go func() {
defer wg.Done()
wg.Done() // ? 多調(diào)用
}()
運(yùn)行直接炸:
panic: sync: negative WaitGroup counter
小結(jié)
Done()本質(zhì)是Add(-1)- 調(diào)用次數(shù)必須嚴(yán)格匹配
坑三:WaitGroup 被復(fù)制(隱蔽但致命)
func worker(wg sync.WaitGroup) { // ? 傳值
defer wg.Done()
}
問(wèn)題:
WaitGroup內(nèi)部有狀態(tài)- 傳值會(huì)復(fù)制一份
- 主 goroutine 等的是原始 wg,子 goroutine 操作的是副本
結(jié)果:永遠(yuǎn)等不到結(jié)束
正確寫(xiě)法:
func worker(wg *sync.WaitGroup)
小結(jié)
WaitGroup 必須用指針傳遞
坑四:WaitGroup 重用不當(dāng)
wg.Add(1)
go func() {
defer wg.Done()
}()
wg.Wait()
wg.Add(1) // ? 有風(fēng)險(xiǎn)
如果之前的 goroutine 還沒(méi)完全結(jié)束,可能出現(xiàn)競(jìng)態(tài)問(wèn)題
建議
一個(gè) WaitGroup 對(duì)應(yīng)一批任務(wù),不要復(fù)用
底層原理解析(重點(diǎn))
WaitGroup 看起來(lái)簡(jiǎn)單,但內(nèi)部實(shí)現(xiàn)非常精妙。
核心結(jié)構(gòu)(簡(jiǎn)化理解):
type WaitGroup struct {
state1 [3]uint32
}
實(shí)際包含:
- counter(計(jì)數(shù)器)
- waiter(等待者數(shù)量)
- semaphore(信號(hào)量)
Add 的本質(zhì)
wg.Add(n)
本質(zhì)是:
原子操作增加計(jì)數(shù)器
atomic.AddInt32(&counter, n)
Done 的本質(zhì)
wg.Done()
等價(jià)于:
wg.Add(-1)
Wait 的本質(zhì)
wg.Wait()
核心邏輯:
- 如果 counter == 0 → 直接返回
- 如果 > 0 → 當(dāng)前 goroutine 阻塞
- 通過(guò)信號(hào)量(semaphore)掛起
喚醒機(jī)制
當(dāng)最后一個(gè) Done() 執(zhí)行:
- counter 變?yōu)?0
- 喚醒所有等待的 goroutine
使用的是 runtime 層的信號(hào)量機(jī)制(runtime_Semrelease)
思考點(diǎn)
為什么 WaitGroup 不用 channel 實(shí)現(xiàn)?
因?yàn)椋?/p>
- channel 需要額外 goroutine 管理
- WaitGroup 使用原子操作 + 信號(hào)量,更輕量、更高效
WaitGroup vs channel vs context
這是很多人容易混淆的點(diǎn)。
WaitGroup
- 用途:等待一組任務(wù)完成
- 不傳遞數(shù)據(jù)
- 不支持取消
channel
- 用途:通信 + 同步
- 可以傳遞數(shù)據(jù)
- 更靈活,但更復(fù)雜
context
- 用途:控制生命周期(取消 / 超時(shí))
- 常用于請(qǐng)求級(jí)控制
小結(jié)
| 工具 | 作用 |
|---|---|
| WaitGroup | 等待任務(wù)結(jié)束 |
| channel | 通信 + 同步 |
| context | 取消 / 控制生命周期 |
最佳實(shí)踐(非常重要)
Add 和 goroutine 啟動(dòng)要“綁定”
wg.Add(1)
go func() {
defer wg.Done()
}()
永遠(yuǎn)使用 defer Done
避免遺漏:
defer wg.Done()
不要跨函數(shù)濫用 WaitGroup
建議:
- 作為參數(shù)傳遞(指針)
- 控制作用域清晰
與 channel 組合使用
WaitGroup 等待結(jié)束,channel 傳遞結(jié)果:
這是生產(chǎn)環(huán)境最常見(jiàn)組合
不要用 WaitGroup 做這些事
- 控制并發(fā)數(shù) ?(應(yīng)該用帶緩沖 channel 或 semaphore)
- 做任務(wù)取消 ?(應(yīng)該用 context)
總結(jié)
WaitGroup 看起來(lái)只是三個(gè)方法,但背后是 Go 并發(fā)設(shè)計(jì)的一個(gè)重要思想:
用最簡(jiǎn)單的機(jī)制解決最單一的問(wèn)題
它的定位非常明確:
- 不負(fù)責(zé)通信
- 不負(fù)責(zé)控制
- 只負(fù)責(zé)等待
也正因?yàn)槿绱耍拍茏龅剑?/p>
- 高性能(原子操作 + 信號(hào)量)
- 低復(fù)雜度
- 易組合(配合 channel / context)
最后的思考
如果讓你自己設(shè)計(jì)一個(gè) WaitGroup,你會(huì)怎么做?
- 用 channel?
- 用鎖?
- 如何避免競(jìng)態(tài)?
想明白這個(gè)問(wèn)題,你對(duì) Go 并發(fā)的理解會(huì)再上一個(gè)層次。
如果你真的從零設(shè)計(jì)一套,你最終會(huì)得到一個(gè)結(jié)論:
WaitGroup 本質(zhì) = 計(jì)數(shù)器 + 阻塞機(jī)制 + 喚醒機(jī)制
WaitGroup 的本質(zhì)是什么?
你可以直接答:
它是一個(gè)基于原子計(jì)數(shù)器的并發(fā)同步原語(yǔ), 通過(guò) runtime 信號(hào)量實(shí)現(xiàn) goroutine 的阻塞與喚醒, 用于解決多個(gè)并發(fā)任務(wù)的收斂(join)問(wèn)題。
以上就是Go等待協(xié)程之WaitGroup使用與避坑指南的詳細(xì)內(nèi)容,更多關(guān)于Go WaitGroup使用與避坑的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
通過(guò)Golang實(shí)現(xiàn)linux命令ls命令(命令行工具構(gòu)建)
這篇文章主要為大家詳細(xì)介紹了如何通過(guò)Golang實(shí)現(xiàn)一個(gè)linux命令ls命令(命令行工具構(gòu)建),文中的示例代碼講解詳細(xì),具有一定的學(xué)習(xí)價(jià)值,感興趣的可以了解一下2023-01-01
golang創(chuàng)建文件目錄os.Mkdir,os.MkdirAll的區(qū)別說(shuō)明
本文主要講述os.Mkdir、os.MkdirAll區(qū)別以及在創(chuàng)建文件目錄過(guò)程中的一些其他技巧,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-03-03
如何在Go語(yǔ)言中靈活運(yùn)用匿名函數(shù)和閉包
這篇文章主要為大家介紹了如何在Go語(yǔ)言中靈活運(yùn)用匿名函數(shù)和閉包實(shí)現(xiàn)實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10
Go利用embed包實(shí)現(xiàn)靜態(tài)資源嵌入的方案
在很多?Go?項(xiàng)目中,程序除了核心代碼之外,往往還需要依賴(lài)一些靜態(tài)資源,傳統(tǒng)做法通常是把這些文件與程序一起打包部署,但這樣會(huì)帶來(lái)一些問(wèn)題,例如部署復(fù)雜、路徑管理困難、發(fā)布時(shí)需要攜帶額外文件等,因此本文給大家介紹了Go利用embed包實(shí)現(xiàn)靜態(tài)資源嵌入的方案2026-03-03
Go語(yǔ)言基礎(chǔ)類(lèi)型及常量用法示例詳解
這篇文章主要為大家介紹了Go語(yǔ)言基礎(chǔ)類(lèi)型及常量的用法及示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助2021-11-11
Go語(yǔ)言開(kāi)發(fā)中redis的使用詳解
這篇文章主要介紹了Go語(yǔ)言開(kāi)發(fā)中redis的使用詳解,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-07-07
Go底層之string和[]byte相互轉(zhuǎn)換原理分析
這篇文章主要介紹了Go底層之string和[]byte相互轉(zhuǎn)換原理,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2025-06-06

