最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Go等待協(xié)程之WaitGroup使用與避坑指南

 更新時(shí)間:2026年04月23日 08:23:35   作者:XMYX-0  
在Go并發(fā)編程中,goroutine非常輕量,但如何優(yōu)雅地等待多個(gè)協(xié)程執(zhí)行完成,才是工程實(shí)踐中的關(guān)鍵問(wèn)題,很多人第一反應(yīng)是用 channel,但其實(shí)sync.WaitGroup才是更合適的工具,這篇文章不僅講用法,還會(huì)帶你深入理解WaitGroup的本質(zhì)、實(shí)現(xiàn)機(jī)制,需要的朋友可以參考下

引言

在 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)文章

  • Go?gRPC服務(wù)客戶(hù)端流式RPC教程

    Go?gRPC服務(wù)客戶(hù)端流式RPC教程

    這篇文章主要為大家介紹了Go?gRPC服務(wù)客戶(hù)端流式RPC教程示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-06-06
  • 通過(guò)Golang實(shí)現(xiàn)linux命令ls命令(命令行工具構(gòu)建)

    通過(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ō)明

    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實(shí)現(xiàn)Redis連接池方法

    Go實(shí)現(xiàn)Redis連接池方法

    為了更深入了解golang連接池的實(shí)現(xiàn),自已又重寫(xiě)了一遍連接池。文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-08-08
  • 如何在Go語(yǔ)言中靈活運(yùn)用匿名函數(shù)和閉包

    如何在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利用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
  • golang中獲取變量類(lèi)型的方法總結(jié)

    golang中獲取變量類(lèi)型的方法總結(jié)

    golang中是沒(méi)有提供內(nèi)置函數(shù)來(lái)獲取變量的類(lèi)型的,但是通過(guò)一定的方式也可以獲取,下面主要給大家介紹了幾個(gè)golang獲取變量類(lèi)型的幾種方式,需要的朋友可以參考下
    2025-03-03
  • Go語(yǔ)言基礎(chǔ)類(lèi)型及常量用法示例詳解

    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的使用詳解

    這篇文章主要介紹了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)換原理分析

    這篇文章主要介紹了Go底層之string和[]byte相互轉(zhuǎn)換原理,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2025-06-06

最新評(píng)論

高台县| 五台县| 乐山市| 万荣县| 田东县| 扶余县| 汨罗市| 乌什县| 松阳县| 辽阳市| 辽宁省| 东乌珠穆沁旗| 广水市| 屯昌县| 息烽县| 绿春县| 天祝| 达拉特旗| 芜湖县| 石狮市| 河源市| 惠东县| 辽阳市| 明水县| 山西省| 柞水县| 东明县| 丰县| 门源| 南溪县| 格尔木市| 竹溪县| 三穗县| 云梦县| 遵义县| 永春县| 元阳县| 贞丰县| 乌兰浩特市| 木兰县| 邵东县|