Golang 中的 WaitGroup 如何使用,面試官經(jīng)常考你
什么是 WaitGroup
sync.WaitGroup
是 Go 語言標(biāo)準(zhǔn)庫中提供的一個(gè)并發(fā)控制工具,用于等待一組 goroutine 完成執(zhí)行。它本質(zhì)上是一個(gè)計(jì)數(shù)器,可以跟蹤正在運(yùn)行的 goroutine 數(shù)量。
WaitGroup 有三個(gè)主要方法:
Add(delta int)
- 增加或減少等待的 goroutine 數(shù)量
Done()
- 相當(dāng)于 Add(-1),表示一個(gè) goroutine 已完成
Wait()
- 阻塞直到計(jì)數(shù)器歸零
基本用法
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // goroutine 完成時(shí)計(jì)數(shù)器減1
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second) // 模擬工作
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1) // 每啟動(dòng)一個(gè) goroutine 計(jì)數(shù)器加1
go worker(i, &wg)
}
wg.Wait() // 等待所有 goroutine 完成
fmt.Println("All workers completed")
}
WaitGroup 的常見誤用
1. 在 goroutine 內(nèi)部調(diào)用 Add
// 錯(cuò)誤用法
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
go func() {
wg.Add(1) // 可能在 Wait 之后才執(zhí)行
defer wg.Done()
// 工作代碼
}()
}
wg.Wait()
正確做法:在啟動(dòng) goroutine 前調(diào)用 Add
2. Add 和 Wait 的調(diào)用順序問題
// 錯(cuò)誤用法
var wg sync.WaitGroup
wg.Wait() // 可能直接通過,沒有等待
wg.Add(1)
go func() {
defer wg.Done()
// 工作代碼
}()
正確做法:確保所有 Add 調(diào)用在 Wait 之前完成
3. 傳遞 WaitGroup 值而非指針
// 錯(cuò)誤用法
func worker(wg sync.WaitGroup) {
defer wg.Done()
// 工作代碼
}
var wg sync.WaitGroup
wg.Add(1)
go worker(wg) // 值傳遞,Done 不影響原始 WaitGroup
wg.Wait() // 永遠(yuǎn)等待
正確做法:傳遞 WaitGroup 指針
4. 計(jì)數(shù)器歸零后重用
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
}()
wg.Wait()
wg.Add(1) // 錯(cuò)誤: 在 Wait 之后再次使用
正確做法:WaitGroup 不能安全地重用,需要新建一個(gè)
WaitGroup + Channel 組合
結(jié)合 WaitGroup 和 Channel 可以實(shí)現(xiàn)更復(fù)雜的并發(fā)控制模式。
1. 限制并發(fā)數(shù)量
func main() {
var wg sync.WaitGroup
ch := make(chan struct{}, 3) // 限制并發(fā)數(shù)為3
for i := 0; i < 10; i++ {
wg.Add(1)
ch <- struct{}{} // 獲取令牌
go func(i int) {
defer wg.Done()
defer func() { <-ch }() // 釋放令牌
fmt.Printf("Worker %d starting\n", i)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", i)
}(i)
}
wg.Wait()
}
2. 收集 goroutine 結(jié)果
func worker(id int, wg *sync.WaitGroup, resChan chan<- string) {
defer wg.Done()
// 模擬工作
time.Sleep(time.Second)
resChan <- fmt.Sprintf("result from worker %d", id)
}
func main() {
var wg sync.WaitGroup
resChan := make(chan string, 10)
// 啟動(dòng)多個(gè) worker
for i := 0; i < 5; i++ {
wg.Add(1)
go worker(i, &wg, resChan)
}
// 等待所有 worker 完成
go func() {
wg.Wait()
close(resChan) // 所有結(jié)果收集完畢后關(guān)閉 channel
}()
// 處理結(jié)果
for res := range resChan {
fmt.Println(res)
}
}
高級(jí)用法:動(dòng)態(tài)調(diào)整并發(fā)數(shù)
func dynamicWorker(id int, wg *sync.WaitGroup, sem chan struct{}) {
defer wg.Done()
fmt.Printf("Worker %d started\n", id)
time.Sleep(time.Second * 2)
fmt.Printf("Worker %d finished\n", id)
<-sem // 釋放信號(hào)量
}
func main() {
var wg sync.WaitGroup
maxConcurrent := 3
sem := make(chan struct{}, maxConcurrent)
for i := 0; i < 10; i++ {
sem <- struct{}{} // 獲取信號(hào)量
wg.Add(1)
go dynamicWorker(i, &wg, sem)
}
wg.Wait()
fmt.Println("All workers completed")
}
總結(jié)
WaitGroup 是 Go 中管理 goroutine 生命周期的強(qiáng)大工具,但需要注意:
- 總是使用指針傳遞 WaitGroup
- 在啟動(dòng) goroutine 前調(diào)用 Add
- 不要重用 WaitGroup
- 結(jié)合 Channel 可以實(shí)現(xiàn)更精細(xì)的并發(fā)控制
通過合理使用 WaitGroup 和 Channel 的組合,可以構(gòu)建出高效且安全的并發(fā)程序。