应为每个测试函数独立检查 goroutine 泄漏:开头记录 runtime.NumGoroutine() 基线,结尾比对差值;配合 sync.WaitGroup 与 t.Cleanup 确保启动的 goroutine 被等待,必要时加 context 超时控制。

如何用 testing.T 捕获 goroutine 泄漏
并发测试最常被忽略的问题不是逻辑错,而是 goroutine 没结束就退出测试,导致后续测试被干扰或 panic。Go 的 testing 包本身不检测 goroutine 生命周期,必须手动控制。
推荐做法是:在测试函数开头调用 runtime.NumGoroutine() 记录基线,测试结束后再次获取并比对。差值不为 0 就说明有泄漏。
- 别只在
TestMain里做一次全局检查——多个测试共用一个*testing.M,goroutine 可能跨测试累积 - 每个测试函数都应独立检查,且最好加超时(比如用
time.AfterFunc打印当前活跃 goroutine 堆栈) - 注意第三方库(如
http.Server、sync.Pool内部 goroutine)可能引入假阳性,可排除已知稳定协程数
用 sync.WaitGroup + t.Cleanup 管理测试中的 goroutine
手动 go func() { ... }() 后忘记 wait 是常见错误。直接在测试中启动 goroutine 时,必须确保它们能被可靠等待或取消。
t.Cleanup 是 Go 1.14+ 提供的机制,适合绑定资源清理逻辑。配合 sync.WaitGroup 可让 goroutine 启动和回收成对出现:
立即学习“go语言免费学习笔记(深入)”;
func TestConcurrentUpdate(t *testing.T) {
var wg sync.WaitGroup
t.Cleanup(func() { wg.Wait() })
<pre class="brush:php;toolbar:false;">for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
// do work...
}(i)
}}
- 不能把
wg.Wait()放在t.Cleanup外面——它会阻塞测试结束,但无法防止 goroutine 在测试中途 panic 后残留 - 如果 goroutine 内部可能阻塞(如等 channel),需额外加 context 控制超时,否则
wg.Wait()会永久挂起 -
t.Cleanup中的函数按后进先出执行,适合嵌套资源释放
测试 channel 关闭与 select 超时行为
并发逻辑大量依赖 select 和 channel,但测试时容易忽略关闭时机和默认分支的竞争条件。
典型陷阱是:向已关闭的 channel 发送数据 panic,或从已关闭 channel 读取时没意识到返回零值+ok==false。测试要覆盖这些边界:
- 用
make(chan T, 1)带缓冲 channel 模拟“瞬时积压”,避免因发送方太快导致接收方还没启动就丢消息 - 测试
select时显式构造超时:case ,而不是依赖外部 sleep;否则在 CI 环境下容易 flaky - 对 close(channel) 的调用点,务必验证所有接收方是否已退出——可通过在接收 goroutine 结束前向某个 done channel 发信号来断言
避免 time.Sleep 做同步,改用 channel 或 atomic
用 time.Sleep 等待 goroutine 执行完是最不可靠的测试写法。它既慢又不稳定:本地快、CI 慢、CPU 负载高时还可能失败。
真正可控的同步方式只有三种:
- channel 通信:发送方写完后 close 或发 token,接收方收到即确认完成
-
sync.WaitGroup:适用于已知数量的 goroutine 启动/结束场景 -
atomic.Bool或atomic.Int64:适用于简单状态标记(如 “worker 已开始”、“handler 已处理第 N 条”)
例如验证一个后台 ticker 是否触发了某次处理:
var count atomic.Int64
ticker := time.NewTicker(10 * time.Millisecond)
go func() {
for range ticker.C {
count.Add(1)
if count.Load() >= 2 {
ticker.Stop()
return
}
}
}()
// 不 sleep,而是轮询原子变量
for count.Load() < 2 {
runtime.Gosched()
}真正难测的从来不是并发逻辑本身,而是你没意识到哪些状态需要被观测、哪些 channel 关闭顺序会影响结果、以及测试环境和生产环境之间那几毫秒的调度差异。










