需用 sync.WaitGroup 确保测试等待所有 goroutine 完成:启动前 wg.Add(n),每个 goroutine 结尾 defer wg.Done(),测试末尾 wg.Wait();避免依赖 time.Sleep;验证并发可配合带缓冲 channel 统一收发信号。
testing 包验证 goroutine 是否真正并发执行单纯启动多个 go func() {}() 并不保证并发行为被测试捕获——主线程可能在 goroutine 执行前就退出。关键是要让测试等待所有 goroutine 完成,同时避免竞态误判。
推荐组合使用 sync.WaitGroup 和 time.Sleep(仅用于调试)或更可靠的信号同步:
WaitGroup.Add(n) 在启动 goroutine 前调用,defer wg.Done() 在每个 goroutine 结尾调用wg.Wait() 阻塞直到全部完成time.Sleep 作为主要同步手段,它不可靠且拖慢测试chan struct{} 让所有 goroutine 发送一次信号,再统一检查接收数量channel 是 goroutine 通信的核心载体,测试重点不是「有没有发」,而是「发得准不准、收得全不全」。
常见错误包括:向已关闭的 channel 发送、从已关闭且无数据的 channel 接收、漏收或重复收。实操建议:
close(ch) 显式关闭(而非仅让 goroutine 退出),接收端用 val, ok := 判断是否关闭
for range ch 自动处理关闭,但需确保发送端确实关闭了 channellen(ch) 返回当前队列长度,cap(ch) 返回容量,二者差值才是剩余空间select 配合 default 做非阻塞收发——这会掩盖阻塞问题,应让测试暴露死锁func TestChannelSendReceive(t *testing.T) {
ch := make(chan int, 2)
go func() {
ch <- 1
ch <- 2
close(ch) // 必须关闭,否则 range 会永远阻塞
}()
var received []int
for v := range ch {
received = append(received, v)
}
if len(received) != 2 || received[0] != 1 || received[1] != 2 {
t.Errorf("expected [1 2], got %v", received)
}}
如何发现并复现 data race(竞态条件)
Go 的 -race 检测器是唯一可靠手段,静态分析或人工 review 几乎无法覆盖所有路径。
启用方式简单,但容易忽略细节:
go test -race,不是 go run -race(后者不生效)counter++ 这种操作,在并发下也是未定义行为append 也会触发 race 报告time.Sleep 强行制造交错执行,反而可能掩盖 race(因为调度变慢),应依赖 -race 自动检测goroutine 泄漏不会立刻报错,但会导致测试进程 hang 住或内存持续增长。Go 测试默认有 10 分钟超时,但应主动设更短的约束。
两个关键动作必须做:
t.Parallel() 时,确保没有全局状态污染;否则并发测试间会相互干扰context.WithTimeout 包
runtime.NumGoroutine() 快速比对前后数量,若明显增多,大概率存在泄漏http.DefaultClient 等全局对象发起的请求,其底层 goroutine 可能延迟退出,需显式调用 CloseIdleConnections()
并发测试里最麻烦的从来不是语法,而是「你以为它结束了,其实它还在跑」——每次加新 goroutine,都要问一句:谁关 channel?谁调 Done()?谁负责回收?