recover必须在defer函数中直接调用才有效,否则恒返回nil;仅能捕获同goroutine中panic传播时的值,且需先注册defer再触发panic,跨goroutine无效。

recover 必须在 defer 里调用,否则永远返回 nil
测试 panic 的本质,是验证 recover() 是否成功捕获到 panic 值。但 recover() 只有在 defer 函数中、且 panic 尚未完全展开时才有效。直接在测试函数体里写 recover(),哪怕紧跟在 panic() 后面,也一定返回 nil——因为 panic 已结束,栈已 unwind 完毕。
常见错误写法:
func TestBad(t *testing.T) {
panic("boom")
r := recover() // 这行根本不会执行
}
正确结构必须是:
- 先注册
defer func() { r := recover(); ... }() - 再触发被测函数(它内部会 panic)
- 最后校验
r是否非空、类型是否匹配、内容是否符合预期
断言 panic 值要先判空再类型转换,别直接强转
recover() 返回 interface{},可能为 nil(没 panic 或 recover 失效),也可能为任意类型值(string、error、自定义 struct 等)。直接写 r.(error) 或 r.(string) 非常危险:一旦类型不匹配,测试本身就会 panic,而不是失败。
安全做法分两步:
- 先用
assert.NotNil(t, r)或if r == nil { t.Fatal("expected panic but none occurred") }确认 panic 确实发生了 - 再用安全类型断言:例如
err, ok := r.(error),然后assert.True(t, ok)和assert.Equal(t, "xxx", err.Error())
如果 panic 传的是字符串,可直接 assert.Equal(t, "expected msg", r),前提是已确认 r != nil。
子 goroutine 中的 panic 无法被外层 recover 捕获
这是最隐蔽也最容易踩的坑。如果被测函数启动了新 goroutine 并在其中 panic,主 goroutine 的 defer + recover 完全无效。现象是:测试卡住、超时,或直接崩溃退出,且无任何 recover 日志。
原因:每个 goroutine 有独立的调用栈,recover() 只能捕获当前 goroutine 的 panic。
解决方式只有两种:
- 把 goroutine 内部逻辑抽出来单独测试(不启 goroutine)
- 在 goroutine 内部加自己的
defer func() { recover() }()
真实场景如 HTTP handler、worker pool 测试时,务必检查是否隐含 goroutine 启动逻辑。
调试 recover 失效?用 runtime/debug.Stack() 打印真实 panic 位置
当 recover() 返回 nil,但你确信 defer 已注册、panic 也该发生,大概率是 panic 发生在别的 goroutine,或 defer 函数里又 panic 了(导致 recover 被跳过)。
这时唯一可靠手段是:在 defer 函数里加堆栈打印:
defer func() {
if r := recover(); r != nil {
buf := make([]byte, 2048)
runtime.Stack(buf, false)
t.Log("panic stack:", string(buf))
}
}()
它能明确告诉你 panic 真正发生在哪个 goroutine、哪一行,而不是靠猜。
复杂点在于:recover 行为高度依赖执行上下文(goroutine、调用栈状态、defer 注册时机),稍有偏差就失效;最容易被忽略的是——panic 值类型不确定时,不做安全断言就直接强转,会让测试崩溃而非失败。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











