waitgroup 不提供内存一致性保障,仅负责计数与阻塞;其 wait() 不等价于内存屏障,无法保证子 goroutine 写入对主 goroutine 可见,必须配合 channel、mutex 或 atomic 显式同步。

WaitGroup 本身不提供内存一致性保障,它只负责计数和阻塞,goroutine 间的数据同步必须靠额外手段(如 channel、mutex 或 explicit memory ordering)。
WaitGroup.Wait() 不等于内存屏障
很多人误以为 wg.Wait() 返回后,所有已执行 wg.Done() 的 goroutine 写入的变量就对主 goroutine “可见”。这是错的。Go 内存模型不保证这点:WaitGroup 的等待逻辑只依赖原子计数器变化,不插入任何 memory fence 或 acquire/release 语义。
- 现象:主 goroutine 在
wg.Wait()后读到零值或旧值,即使子 goroutine 已写入并调用wg.Done() - 原因:编译器重排或 CPU 缓存未刷新,且
WaitGroup未强制同步该路径上的内存访问 - 正确做法:若需传递数据,必须显式同步 —— 用
channel发送结果,或用sync.Mutex保护共享变量,或用atomic.Store/atomic.Load配合atomic.Add等
wg.Add() 必须在 go 启动前调用
这是最常踩的坑。wg.Add(1) 和 go f() 的顺序直接影响竞态是否发生。
- 错误写法:
go func() { wg.Add(1); ...; wg.Done() }()——Add在 goroutine 内部,主 goroutine 可能在Wait()前看到计数器仍为 0,提前返回 - 正确写法:始终在
go语句之前调用wg.Add(1),或在循环中先wg.Add(1)再go - 根本原因:Go 内存模型要求
Add对Wait可见,而go语句隐含 happens-before 关系,但仅限于启动瞬间;延迟的Add无法参与该链
WaitGroup 不可重用且不能拷贝
这两个限制都源于其内部结构设计,违反会导致 panic 或静默错误。
-
noCopy字段使go vet能检测浅拷贝 —— 比如传值给函数、赋值给新变量、作为 struct 字段嵌入后复制,都会触发警告 - 不可重用:一旦
wg.Wait()返回(计数器归零),再次调用wg.Add()是未定义行为;官方明确不支持,某些版本会 panic,某些则行为异常 - 修复方式:需要重复使用时,应声明新
sync.WaitGroup实例,或封装成函数内局部变量
并发安全 ≠ 内存安全
WaitGroup 的方法(Add、Done、Wait)是并发安全的,但这只指“自身计数器不会损坏”,不延伸至用户数据。
- 常见误区:认为用了
WaitGroup就不用管 map、slice、struct 字段的并发读写 - 真实情况:多个 goroutine 同时写同一
map即使有wg也会 crash;同时读写同一int变量仍可能得到撕裂值 - 关键区分:WaitGroup 解决的是“任务完成通知”,不是“数据访问协调” —— 后者必须用
sync.Map、atomic、mutex或channel
最易被忽略的一点:WaitGroup 的 zero value 是可用的,但它的 state1 字段依赖内存对齐才能正确进行 64 位原子操作;在 CGO 或手动内存布局场景下,若结构体被 misaligned,可能在 32 位平台触发 SIGBUS —— 这类问题往往只在特定部署环境暴露,调试成本极高。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











