每个测试函数必须独立检查 goroutine 泄漏:开头记录 runtime.numgoroutine() 为 before,结束后再取 after 并断言差值为 0;需用 sync.waitgroup + t.cleanup 确保 goroutine 完全退出,避免依赖 sleep;channel 必须显式 close 并正确处理接收状态。

测试函数开头和结尾都要查 runtime.NumGoroutine()
goroutine 泄漏不是“偶尔出问题”,而是每次泄漏都会叠加——上一个测试没清理干净的 goroutine,会算进下一个测试的基线里。所以不能只在 TestMain 里统一开始/结束检查。
每个测试函数必须自己记录差值:
• 开头调一次 runtime.NumGoroutine() 存为 before
• 所有 goroutine 启动、等待、超时逻辑走完后,再调一次得到 after
• 断言 if after != before { t.Fatalf("leaked %d goroutines", after-before) }
第三方库(比如 http.Server 内部常驻的监听 goroutine)可能引入固定偏移,可预先测出稳定值,在断言时减去它
用 sync.WaitGroup + t.Cleanup 管理生命周期
手动起 goroutine 却忘了等它结束,是测试里最典型的泄漏来源。别依赖 time.Sleep,它既不可靠又拖慢 CI。
正确做法是成对使用:
• 在测试函数开头声明 var wg sync.WaitGroup
• 调用 t.Cleanup(func() { wg.Wait() }),确保无论测试成功或 panic 都会等完
• 每个 go func() { ... }() 前调 wg.Add(1),结尾用 defer wg.Done()
注意:如果 goroutine 内部可能卡在 channel 或 network 上,wg.Wait() 会永久阻塞——这时必须加 context.WithTimeout 控制执行边界
验证 channel 关闭与接收完整性
并发测试里,for range ch 看似简洁,但若发送端没显式 close(ch),就会死锁;而从已关闭 channel 接收时返回零值+ok==false,容易误判为“没收到”。
实操要点:
• 发送端务必调 close(ch),而不是仅让 goroutine 退出
• 接收端优先用 val, ok := 显式判断状态,避免隐式零值干扰<br>• 带缓冲 channel(如 <code>make(chan int, 2))更适合模拟瞬时积压,防止因调度顺序导致消息丢失
• 别用 select { default: ... } 做非阻塞收发——这会让测试绕过真实阻塞路径,掩盖死锁风险
并发行为本身需要信号同步,不能靠运气
写 go f1(); go f2() 不等于“它们并发执行了”,主线程可能在它们开始前就退出。要验证并发发生,得靠信号对齐:
• 用带缓冲的 done := make(chan struct{}, 2)
• 每个 goroutine 结束前发一次 done <br>• 主线程用 <code>for i := 0; i 确保两个都完成<br>• 若需验证“同时性”,可加 <code>time.Now() 打点比对时间戳差值,但注意 Go 调度器不保证微秒级精度
真正难的不是写并发逻辑,而是让测试能稳定暴露那些只在特定调度顺序下才出现的竞态——比如 channel 关闭时机、共享变量读写竞争、超时与取消的边界条件。这些点没法靠覆盖率数字体现,得靠每轮测试都强制检查 goroutine 数量、显式同步信号、以及对 channel 状态的穷尽判断。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











