Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > Golang sync.WaitGroup 等待组

Golang中sync.WaitGroup 等待组的实现

作者:FfHUCisI

本文介绍了Go语言中sync.WaitGroup的使用方法和注意事项,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

一、WaitGroup 解决什么问题

当我们启动了一组 goroutine 去干活,主 goroutine 需要在所有子 goroutine 完成之后再继续执行。比如:

WaitGroup 就是为这类"等待一组并发任务完成"的诉求设计的。

二、三个核心方法

WaitGroup 的 API 极为简洁——只有 Add、Done、Wait 三个方法:

var wg sync.WaitGroup

// Add(n): 计数器加 n,表示"我又派了 n 个任务"
wg.Add(3)

for i := 0; i < 3; i++ {
    go func(id int) {
        defer wg.Done() // 等价于 Add(-1),表示"我干完了"
        // ... 干活 ...
    }(i)
}

wg.Wait() // 阻塞直到计数器归零

Done 内部就是 wg.Add(-1),二者语义完全等价。

三、设计哲学:谁派生,谁 Add

这是最容易踩坑的地方。Add 必须在启动 goroutine 之前调用,否则可能 Wait 先执行,直接放行:

// ❌ 错误:wg.Add 在 go 里面
for i := 0; i < 5; i++ {
    go func() {
        wg.Add(1)  // 严重错误!Wait 可能在这之前就返回了
        defer wg.Done()
        doWork()
    }()
}
wg.Wait()

正确的做法是 Add 和 go 语句紧挨在一起:

// ✅ 正确
for i := 0; i < 5; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        doWork()
    }()
}
wg.Wait()

四、WaitGroup 是值类型,必须传指针

WaitGroup 结构体内部维护了一个计数器,不能用做值拷贝的参数传递——副本中的加减不会影响原值。

// ❌ 值传递:计数器的修改对原始 wg 不可见
func run(wg sync.WaitGroup) {
    defer wg.Done()
}

// ✅ 指针传递
func run(wg *sync.WaitGroup) {
    defer wg.Done()
}

Go 1.20+ 编译器会给出 copylock 警告,但不如一开始就用对。

五、WaitGroup 不能重用"飞行中"的实例

WaitGroup 的内部状态有三个阶段:0 → Add → Wait。Wait 返回后,WaitGroup 自动回到 0 状态可以重用。但如果在 Wait 还没返回时又调用 Add,这属于并发访问内部状态,是数据竞争。

// ❌ 数据竞争!
go func() {
    wg.Add(1) // 与下面的 wg.Wait 并发
    defer wg.Done()
    doWork()
}()
wg.Wait() // 此时可能还没有 Add 完成

六、练习代码

保存为 waitgroup_demo.go

package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

// 模拟从多个数据源获取数据
type DataFetcher struct {
    name    string
    latency time.Duration // 模拟耗时
}

func (f DataFetcher) Fetch() string {
    time.Sleep(f.latency)
    return fmt.Sprintf("[%s 数据]", f.name)
}

func main() {
    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))
    var wg sync.WaitGroup

    for i, src := range sources {
        wg.Add(1) // 在 go 之前 Add
        go func(idx int, fetcher DataFetcher) {
            defer wg.Done()
            results[idx] = fetcher.Fetch()
        }(i, src) // 注意:把循环变量传给闭包
    }

    fmt.Println("等待所有数据源返回...")
    wg.Wait()
    fmt.Println("全部就绪!\n")

    for i, r := range results {
        fmt.Printf("数据源 %d (%s): %s\n", i, sources[i].name, r)
    }

    // —— 进阶:带超时的 WaitGroup ——
    fmt.Println("\n——— 超时模式演示 ———")

    var wg2 sync.WaitGroup
    wg2.Add(1)
    go func() {
        defer wg2.Done()
        time.Sleep(2 * time.Second) // 模拟很慢的任务
        fmt.Println("慢任务完成")
    }()

    // 用一个 channel 来通知"WaitGroup 已经等完了"
    done := make(chan struct{})
    go func() {
        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 语句之前调用 Add
defer Done确保 goroutine panic 也能 Done
传指针WaitGroup 是值类型,传递需取地址
Wait 可重入多个 goroutine 可以同时 Wait
超时模式Wait 本身不超时,配合 channel + select 实现
不能复制复制 WaitGroup 的值会破坏内部状态

到此这篇关于Golang中sync.WaitGroup 等待组的实现的文章就介绍到这了,更多相关Golang sync.WaitGroup 等待组内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文