搞懂 Go WaitGroup:一篇文章彻底理解并发等待机制

一个生活中的比喻 🍜

想象你是一个火锅店老板,今天有 3 桌客人同时点了菜:

  • 第 1 桌:点了毛肚
  • 第 2 桌:点了鸭肠
  • 第 3 桌:点了虾滑

你喊了 3 个服务员同时 去准备这三桌的菜。作为老板,你需要确认所有服务员都上完菜之后,才能在系统里点击"本轮上菜完毕"。

这就是 WaitGroup 的作用------等待一组并发任务全部完成


WaitGroup 是什么?

sync.WaitGroup 是 Go 标准库提供的一个并发同步工具。它本质上就是一个计数器

方法 作用 火锅店比喻
Add(n) 计数器 +n,告诉它有 n 个任务要做 老板说:"有 3 桌菜要上"
Done() 计数器 -1,表示一个任务完成了 服务员说:"我这桌搞定了"
Wait() 阻塞等待,直到计数器归零 老板等着,直到所有服务员都回来

没有 WaitGroup 会怎样?(翻车现场)

go 复制代码
package main

import (
    "fmt"
    "time"
)

func serve(table int, dish string) {
    time.Sleep(time.Second) // 模拟上菜需要1秒
    fmt.Printf("第 %d 桌的 %s 上好了!\n", table, dish)
}

func main() {
    go serve(1, "毛肚")
    go serve(2, "鸭肠")
    go serve(3, "虾滑")

    fmt.Println("老板:本轮上菜完毕!")
    // 💀 程序直接结束了,服务员还没上完菜呢!
}

运行结果:

复制代码
老板:本轮上菜完毕!

菜呢??没了!因为 main 函数一结束,整个程序就退出了,3 个协程还没来得及执行完。

这就好比老板还没等服务员上完菜,就直接关店走人了。😅


用 WaitGroup 拯救世界

go 复制代码
package main

import (
    "fmt"
    "sync"
    "time"
)

func serve(table int, dish string, wg *sync.WaitGroup) {
    defer wg.Done() // 上完菜就报告:我搞定了!(计数器-1)
    time.Sleep(time.Second)
    fmt.Printf("第 %d 桌的 %s 上好了!\n", table, dish)
}

func main() {
    var wg sync.WaitGroup

    wg.Add(3) // 老板说:有3桌菜要上

    go serve(1, "毛肚", &wg)
    go serve(2, "鸭肠", &wg)
    go serve(3, "虾滑", &wg)

    wg.Wait() // 老板在这里等着,直到3个服务员都回来

    fmt.Println("老板:本轮上菜完毕!✅")
}

运行结果:

复制代码
第 2 桌的 鸭肠 上好了!
第 1 桌的 毛肚 上好了!
第 3 桌的 虾滑 上好了!
老板:本轮上菜完毕!✅

注意:前三行的顺序可能每次不同(因为协程是并发执行的),但"上菜完毕"一定在最后!


核心三步走(记住这个就够了)

scss 复制代码
第一步:创建   var wg sync.WaitGroup
第二步:登记   wg.Add(任务数量)
第三步:开工   go 干活(&wg)        ← 干完后调用 wg.Done()
第四步:等待   wg.Wait()           ← 在这阻塞,直到所有任务完成

进阶:循环中使用 WaitGroup

实际开发中,我们通常在循环里启动多个协程:

go 复制代码
package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup

    dishes := []string{"毛肚", "鸭肠", "虾滑", "牛肉", "豆腐"}

    for i, dish := range dishes {
        wg.Add(1) // 每启动一个协程,计数器+1
        go func(table int, food string) {
            defer wg.Done()
            fmt.Printf("第 %d 桌的 %s 上好了!\n", table+1, food)
        }(i, dish)
    }

    wg.Wait()
    fmt.Println("全部上菜完毕!🎉")
}

常见踩坑点 ⚠️

坑1:忘记传指针

go 复制代码
// ❌ 错误:值传递,函数内的 Done() 对外面的 wg 无效
func serve(wg sync.WaitGroup) {
    defer wg.Done()
}

// ✅ 正确:传指针
func serve(wg *sync.WaitGroup) {
    defer wg.Done()
}

坑2:Add 和 Go 的顺序不对

go 复制代码
// ❌ 错误:先启动协程,再 Add ------ 可能 Wait 的时候计数器还是0
go func() {
    wg.Add(1)
    defer wg.Done()
    // 干活...
}()
wg.Wait()

// ✅ 正确:先 Add,再启动协程
wg.Add(1)
go func() {
    defer wg.Done()
    // 干活...
}()
wg.Wait()

坑3:Add 的数量和 Done 的次数不匹配

go 复制代码
wg.Add(3)
// 但只启动了 2 个协程调用 Done()
// 结果:wg.Wait() 永远等下去,程序死锁!💀

WaitGroup vs Channel 该用哪个?

场景 推荐
只需要等待一组任务完成 ✅ WaitGroup
需要从协程中获取返回值 ✅ Channel
需要控制并发数量 ✅ Channel + WaitGroup
任务之间有依赖关系 ✅ Channel

简单记忆:只等不收用 WaitGroup,又等又收用 Channel。


总结

概念 一句话解释
WaitGroup 一个计数器,等所有并发任务完成
Add(n) 告诉它:我要启动 n 个任务
Done() 告诉它:我这个任务完成了
Wait() 阻塞在这,直到所有任务完成

就像火锅店老板等服务员上菜一样简单!🍲

相关推荐
一 乐1 小时前
在线考试|基于Springboot的在线考试管理系统设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·毕设·在线考试管理系统
右耳朵猫AI1 小时前
Golang技术周刊 2026年第20周
开发语言·后端·golang
我是一颗柠檬2 小时前
【Redis】有序集合与位图Day5(2026年)
数据库·redis·后端·缓存
喵了几个咪2 小时前
Headless 后端实践:基于Go的企业级多栈管理系统脚手架
开发语言·vue.js·后端·golang·reactjs·gowind
小小龙学IT2 小时前
Go 并发模式深度解析:Fan-out/Fan-in 高效处理大规模数据流
开发语言·后端·golang
我是一颗柠檬2 小时前
【Redis】持久化机制Day6(2026年)
数据库·redis·后端·缓存·database
Penge66610 小时前
Go 接口编译期断言
后端
我是一颗柠檬10 小时前
【MySQL全面教学】MySQL面试高频考点汇总Day15(2026年)
数据库·后端·mysql·面试
拽着尾巴的鱼儿11 小时前
springboot openfeign 自定义feign 接口重试机制
java·spring boot·后端