应使用 sync.WaitGroup 显式等待 goroutine 完成:启动前 wg.Add(1),结束时 wg.Done(),主协程调用 wg.Wait();channel 由发送方在所有发送完成后关闭;避免 time.Sleep 和未同步的共享变量,启用 -race 检测竞态。
Go 的 testing 包默认不等待 goroutine 结束,go func() { ... }() 启动后主测试函数一返回,整个测试就结束,导致并发逻辑根本没执行完。这不是 bug,是设计使然——goroutine 是异步的,测试框架不会自动同步。
sync.WaitGroup 显式等待所有 goroutine 完成:在启动前 wg.Add(1),每个 goroutine 结束时调用 wg.Done(),主 goroutine 调用 wg.Wait()
time.Sleep 等待,它不可靠、慢、且掩盖真实同步问题WaitGroup 必须在所有 goroutine 启动前完成 Add,否则可能 panic:比如循环中先 go func(i int) 再 wg.Add(1),i 可能被闭包捕获为错误值range 死锁或 panic测试中常通过 channel 收集 goroutine 输出结果,但若关闭过早(如在发送 goroutine 还没发完时就 close(ch)),range ch 会提前退出,漏数据;若关闭过晚或忘记关,range 永远阻塞,测试超时失败。
WaitGroup 确保全部发送完毕后再 close(ch))close(ch)
sync.Once
select + time.After 模拟超时,但实际行为不符合预期写并发测试时,常想“等 100ms,如果没收到结果就报错”。但 select 中的 time.After 每次都会新建一个 timer,若放在循环里反复调用,会产生大量泄漏 timer,且逻辑易错。
timeout := time.After(100 * time.Millisecond) 提到循环外,复用同一个 channelselect 分支中对 channel 的读写不会阻塞其他分支(例如向未缓冲的 channel 发送而无人接收,会导致该 case 永远不满足)context.WithTimeout,配合 ctx.Done(),尤其适合嵌套或可取消的测试场景这类问题往往源于竞态(race):多个 goroutine 无保护地读写同一变量,或依赖未同步的执行顺序。Go 自带 race detector,但必须显式启用,否则编译器不会报错。
-race 标志:go test -race -v ./...
sync.Mutex)或用原子操作(atomic.AddInt64 等)fmt.Println 或 log.Print 做“调试同步”,它们不是同步原语,也不能替代锁并发测试最难的不是写 goroutine
,而是让“不确定性”变得确定:每处共享、每次发送、每个等待点,都要有明确归属和生命周期。漏掉一个 wg.Done(),少关一个 channel,或者多启一个没受控的 goroutine,测试就会在 CI 里间歇性崩掉。