测试recover必须在defer中调用,直接在测试函数体调用recover()永远返回nil;正确做法是先注册defer func(){r=recover()}()再触发panic,且panic须与recover同goroutine。

测试 recover 必须在 defer 里调用,不能在 test 函数体直接 recover()
recover() 只能在 defer 中生效,且必须在 panic 发生的同一 goroutine 内。测试函数里写 recover() 永远返回 nil,因为 panic 已经结束、堆栈已 unwind 完毕。
正确做法是:先注册 defer func() { r := recover(); ... },再触发被测函数 panic。常见错误包括:
- 把
recover()放在panic()后面但不在 defer 里 - 被测函数内部启了 goroutine 并 panic —— 外层 defer 完全捕获不到
- 没判空就直接类型断言,比如
r.(error),panic 值不是 error 时测试会崩溃而非失败
推荐结构:
func TestMyHandler_Panic(t *testing.T) {
var r interface{}
defer func() { r = recover() }()
myHandlerThatPanic()
assert.NotNil(t, r)
assert.Equal(t, "expected msg", r)
}
用 httptest 测试 Gin handler 的 panic 恢复中间件
要验证自定义 recovery 中间件是否生效,得走完整 HTTP 请求链:构造请求 → 走 router.ServeHTTP() → 检查响应状态码和 body。关键点:
- 必须提前调
gin.SetMode(gin.TestMode),否则默认 logger/recovery 会干扰输出 - 中间件顺序必须和生产一致:自定义 recovery 要放在所有可能 panic 的 handler 和中间件之前
- 用
httptest.NewRecorder()捕获响应,别依赖终端日志 - 如果中间件里调了
c.AbortWithStatusJSON(500, ...),响应 body 应该是 JSON;若只调c.AbortWithStatus(500),body 就为空
示例片段:
func TestCustomRecovery(t *testing.T) {
gin.SetMode(gin.TestMode)
r := gin.New()
r.Use(CustomRecovery()) // 你写的中间件
r.GET("/panic", func(c *gin.Context) {
panic("boom")
})
req, _ := http.NewRequest("GET", "/panic", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, 500, w.Code)
assert.JSONEq(t, `{"code":500,"msg":"internal server error"}`, w.Body.String())
}
为什么测试中 recover 总是 nil?优先看 runtime.Stack()
当 recover() 返回 nil 但你确认 panic 确实发生了,大概率是以下两种情况之一:
- panic 发生在子 goroutine 里(比如
go func() { panic(...) }()),主 goroutine 的 defer 无法捕获 - recover 调用时机不对:比如 defer 函数里又 panic 了,或者 panic 后还执行了其他逻辑导致 recover 被跳过
调试时别猜,直接加 debug.Stack() 打印真实堆栈:
defer func() {
if r := recover(); r != nil {
buf := make([]byte, 4096)
n := runtime.Stack(buf, false)
log.Printf("panic stack: %s", buf[:n])
}
}()
这个输出能明确告诉你 panic 真实发生在哪一行、哪个 goroutine,比任何文档都管用。
测试文件上传 handler 的 panic 分支要先让 FormFile 成功返回
很多 panic 出现在 c.FormFile("file") 后的校验逻辑里(比如空文件、超大文件、非法扩展名)。但测试时如果 c.FormFile() 本身返回 nil,业务逻辑根本不会执行,panic 分支也就覆盖不到。
确保 FormFile 成功的关键是:用 bytes.Buffer + multipart.Writer 构造请求体,并调用 writer.Close()。漏掉这一步,c.MultipartForm() 解析失败,后续全白搭。
- 字段名必须和 handler 里
c.FormFile("xxx")的参数完全一致 -
Content-Type头必须用writer.FormDataContentType()的返回值,不能手写 - 空文件测试:用
writer.CreateFormFile("file", "empty.txt")创建空字段,FormFile仍返回非 nil 的*multipart.FileHeader,后续读取才可能 panic
否则你测的根本不是“业务 panic”,而是“Gin 解析失败”。











