WaitGroup.state1用单个uint64原子操作同时更新counter(高32位)和wait count(低32位),Add(1)执行atomic.AddUint64(statep, 1<<32),Wait循环重检高位是否为0以防虚假唤醒,noCopy字段由go vet编译期检测拷贝以避免死锁。

WaitGroup.state1 是如何用单个原子操作更新 counter 和 wait count 的
WaitGroup 把 counter(高 32 位)和 wait count(低 32 位)塞进同一个 uint64 字段 state1,所有读写都走 atomic.AddUint64 或 atomic.CompareAndSwapUint64。这意味着:一次加减、一次比较交换,就能同时修改两个逻辑状态,不需要锁,也不需要两次原子操作。
比如 Add(1) 实际执行的是:atomic.AddUint64(statep, uint64(1) —— 左移 32 位,只动高 32 位;<code>Wait() 在挂起前执行的是:atomic.AddUint64(statep, 1) —— 只动低 32 位。两者互不干扰,但共享同一内存地址,靠位移和掩码隔离。
关键约束在于:不能拆成两步更新。例如,先读再算再写,就可能被其他 goroutine 插入,导致状态错乱。
为什么 Add(-1) 时 panic “negative WaitGroup counter”
这个 panic 出现在 atomic.AddUint64 返回后,对高 32 位做符号检查时:v := int32(state >> 32)。如果 v 就直接 panic。
立即学习“go语言免费学习笔记(深入)”;
常见触发场景包括:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
-
Done()调用次数超过Add(n)总和,比如Add(1)后调用了两次Done() - 多个 goroutine 并发调用
Add(-1),且初始值为 0,导致高 32 位绕回成负数(int32 溢出) - 误在
Wait()还没返回时又调用Add(1),而此时 state 高位为 0、低位 > 0,Add 会尝试把高位设为 1,但源码中有一条校验:if w != 0 && delta > 0 && v == int32(delta),此时会 panic “Add called concurrently with Wait”
Wait 方法里那个 for 循环不是为了重试,而是防虚假唤醒
Wait() 底层调用 runtime_Semacquire(semap) 挂起 goroutine,唤醒时机由 runtime_Semrelease 触发 —— 而后者只在 counter 真正归零时批量调用一次。但唤醒后必须重新读 state1,提取高 32 位判断是否为 0。
原因有三:
- 调度器不保证唤醒与
Done()原子写之间的严格顺序 - 可能多个
Done()连续执行,唤醒发生在中间某次之后,但此时counter还没到 0 - 更隐蔽的情况:刚唤醒,另一个 goroutine 立刻
Add(1),counter又变非零
所以循环体是:for ; atomic.LoadUint64(statep)>>32 != 0; { runtime_Semacquire(semap) } —— 不是“失败重试”,而是“未满足条件就继续等”。
noCopy 字段不是运行时机制,是 vet 编译期告警开关
noCopy 是一个空结构体字段,本身不占内存(Go 编译器会优化掉),唯一作用是让 go vet 在检测到值拷贝时报错,例如:
wg1 := sync.WaitGroup{}
wg2 := wg1 // go vet 会警告:assignment copies lock value to wg2
一旦发生拷贝,两个 WaitGroup 实例就拥有各自独立的 state1 和 sema,Wait() 在 wg1 上阻塞,Done() 却在 wg2 上调用,永远无法唤醒 —— 表现为死锁,且无 panic,极难定位。
容易被忽略的拷贝点:
- 作为函数参数传值(而非指针)
- 在 struct 中嵌入 WaitGroup 并对该 struct 做整体赋值
- 在 map 中以 WaitGroup 为 value 类型(map 赋值会拷贝 value)

















