← 中文资源

简体中文版本

Go 测试 panic: test timed out — goroutine 死锁

通过阅读 Go 打印的 goroutine dump、找出阻塞的 channel 或锁,并为等待加上超时,诊断 “panic: test timed out”。

阅读英文原文

症状

本篇调试指南针对“Go 测试 panic: test timed out — goroutine 死锁”。内容涵盖可观察的症状、可复现的诊断路径、根本原因、具体修复方式,以及如何验证修复并防止回归。

可观察到的症状包括:

  • go test 失败并提示 “panic: test timed out after 10m0s”
  • 堆栈转储中所有 goroutine 都停在 chan receive 或 sync.Mutex
  • 去掉某次发送后测试即通过

诊断

先在隔离环境中复现故障,再按顺序执行以下检查,逐步缩小范围。

步骤 1

go test -run TestX -timeout 15s

崩溃转储会显示每个 goroutine 及其阻塞的确切行,直接指出元凶。

步骤 2

go test -run TestX -timeout 15s 2>&1 | grep -A 8 "goroutine .* \["

按状态分组 goroutine,可判断是发送/接收不匹配还是锁循环。

常见诱因(按出现频率排序):

  • channel 发送没有对应接收,发送方永久阻塞
  • 两个 goroutine 互相等待对方释放锁
  • WaitGroup 计数永不归零,因为某 goroutine 未启动或已 panic

根因

当所有 goroutine 都在等待、无法推进时,Go 会阻塞而非报错。此时 goroutine 转储显示全部停在 channel 操作或互斥锁等待上,据此可将挂起判定为死锁或缺少接收方。

修复

平衡发送与接收,适当为 channel 设缓冲,保证锁的获取顺序一致,并让等待可被 select 配合超时或 context 取消打断。

注意:仅加超时可能掩盖真正的死锁;应修复设计,而非只给等待设上限。

// ❌ unbuffered send with no reader -> deadlock
ch := make(chan int)
ch <- 1 // blocks forever

// ✅ buffered, then read
ch := make(chan int, 1)
ch <- 1
fmt.Println(<-ch)

// ✅ wait with timeout
select {
case v := <-ch:
	fmt.Println(v)
case <-time.After(2 * time.Second):
	t.Fatal("timeout waiting for worker")
}

验证

在 -timeout 下重跑并确认远早于限制完成;用 -race 与 -count=100 捕获间歇性挂起。

修复后补充一条回归测试,复现原始症状,防止问题再次出现。

这类故障通常是环境问题而非纯逻辑错误,因此在缺陷记录中应写明完整命令、环境变量与工作目录。请用最小的脚本复现,待最小用例清楚后再扩大范围。为这类故障准备一份简短的排查清单,能显著减少下次排障时间。