导读:本期聚焦于小伙伴创作的《Go语言协程同步时如何使用sync.WaitGroup避免常见错误?》,敬请观看详情。在主协程启动多个 goroutine 处理任务后直接退出,子任务还没跑完程序就结束了,这是初写 Go 并发最容易踩的坑。sync.WaitGroup 通过计数器机制让主协程阻塞等待一组协程全部返回。核心用法是 Add 设置任务数,Done 在协程末尾递减,Wait 阻塞至计数归零。错误地在循环里用闭包捕获循环变量、忘记调用 Done 导致死锁、或者 Add 传负数引发 panic,都会让同步失效。实际编码中还应配合 defer 保证 Done 执行,并用匿名函数隔离参数。理解计数器的原子操作本质,才能在批量抓取、并行计算等场景写出稳健的并发控制代码。

在 Go 语言里,并发模型依靠轻量的 goroutine 来实现,但当我们需要等待一批协程全部完成再继续后续逻辑时,如果只靠 time.Sleep 或者通道手动计数,代码既脆弱又难维护。sync.WaitGroup 是标准库提供的结构体,它通过内部计数器帮主协程精准阻塞,直到所有子协程报告结束。理解它的方法语义和并发安全性,是写出正确并发程序的基础。

Go语言协程同步时如何使用sync.WaitGroup避免常见错误?

WaitGroup 的基本机制与正确调用顺序

sync.WaitGroup 暴露三个方法:Add、Done 和 Wait。Add 用于增加内部计数器,通常在启动协程前调用,传入即将创建的协程数量;Done 将计数器减一,一般放在协程函数末尾;Wait 会阻塞调用它的协程,直到计数器变为零。底层实现依赖原子操作和信号量,因此多个协程并发调用 Done 是安全的,但 Add 和 Wait 的调用时机有严格要求。

最常见的错误是在启动 goroutine 之后才调用 Add,这样主协程的 Wait 可能先于 Add 执行,导致计数器为零而直接返回,子协程变成孤儿协程。正确写法是在 go 关键字之前完成 Add,或者使用循环统一 Add 后再启动。下面的示例展示了安全模式:

package main

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

func main() {
    var wg sync.WaitGroup
    tasks := []string{"a", "b", "c"}
    for _, t := range tasks {
        wg.Add(1)
        go func(task string) {
            defer wg.Done()
            time.Sleep(100 * time.Millisecond)
            fmt.Println("finished", task)
        }(t)
    }
    wg.Wait()
    fmt.Println("all done")
}

上述代码在每次循环中都先 Add(1),然后立即启动携带参数的匿名协程,并在协程中用 defer 调用 Done。这样即使任务 panic(未被 recover 时除外),Done 也会执行,避免主协程永久阻塞。注意我们显式把循环变量 t 以参数形式传入,而不是在闭包里直接引用,这能防止变量被后续迭代覆盖。

典型误用场景与对应的排查思路

第一种典型误用是忘记调用 Done,或者在某些提前 return 的分支中遗漏。由于 WaitGroup 计数不为零,Wait 永远不返回,程序表现为假死。借助 defer wg.Done() 可以把递减动作绑定到函数退出,不论正常结束还是出错都生效,比在末尾手写更可靠。若协程内存在多重退出点,defer 是唯一省心的做法。

第二种误用是 Add 传入负值或计数器变成负数。WaitGroup 不允许计数器小于零,否则会触发 runtime panic。这通常发生在 Done 调用次数多于 Add,例如重复 defer 或者逻辑错误多减了一次。在复杂调用链中,建议把 Add 集中在父作用域,子函数只负责 Done,并且通过代码评审确认配对关系。

第三种误用是复制 WaitGroup 实例。WaitGroup 内部含不可复制的状态,若将其值传给函数或存入切片,会造成计数器不同步。应始终传递指针,如 *sync.WaitGroup。下面的错误示例演示了值传递带来的隐患:

package main

import (
    "sync"
)

func bad(wg sync.WaitGroup) {
    // 这里操作的是副本,原 wg 计数器不变
    defer wg.Done()
}

func main() {
    var wg sync.WaitGroup
    wg.Add(1)
    bad(wg)
    wg.Wait() // 永久阻塞
}

将参数改为 *sync.WaitGroup 并传 &wg 即可修复。Go 的 vet 工具能静态检查出部分值复制问题,日常开发应把 go vet 纳入流水线。

在真实业务中的组合实践与替代方案

在并行抓取或批量处理数据库记录时,WaitGroup 常与带缓冲通道组合,既控制并发上限又等待全部完成。例如启动 N 个 worker,从任务通道读取,处理完调用 Done,主协程 Wait 后关闭资源。这种结构比无限制开启 goroutine 更平稳,不会因为任务量暴涨而耗尽内存。

当任务需要返回结果或错误时,可以配合 errgroup.Group 使用,它在 WaitGroup 基础上增加了错误传播和上下文取消能力。如果场景只是简单的“等所有都做完”,原生 WaitGroup 更轻量;若需要任意一个失败就取消其余协程,errgroup 更合适。下面展示与通道限流结合的写法:

package main

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

func main() {
    var wg sync.WaitGroup
    sem := make(chan struct{}, 3)
    for i := 0; i < 10; i++ {
        wg.Add(1)
        sem <- struct{}{}
        go func(id int) {
            defer wg.Done()
            defer func() { <-sem }()
            time.Sleep(time.Second)
            fmt.Println("task", id)
        }(i)
    }
    wg.Wait()
    close(sem)
}

这段程序最多同时运行三个协程,既利用了多核又保护了对端服务。WaitGroup 只负责等待,限流交给 sem 通道,职责清晰。实践中还应考虑加上 context 以便超时退出,避免外部信号无法中断 Wait。掌握这些组合方式后,sync.WaitGroup 就能在绝大多数协程同步需求中提供简洁可靠的解决方案。

sync.WaitGroupgoroutineGo并发修改时间:2026-08-15 13:09:32

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。