recover必须在defer中调用才能生效,且需在可能panic的代码前注册;仅捕获同goroutine panic;recover后无法回滚已执行副作用;返回值类型不确定,应谨慎处理。

recover必须在defer中调用,否则永远返回nil
文件解析(比如 JSON/YAML/CSV 解析)常因输入非法触发 panic,例如 json.Unmarshal 本身不会 panic,但若传入未初始化的 nil 指针、或结构体字段有 panic 触发的自定义 UnmarshalJSON 方法,就可能崩。此时想“兜底”,第一反应是加 recover —— 但直接写 recover() 就失效。
常见错误写法:
func parseFile(path string) error {
recover() // ← 这行毫无作用,永远返回 nil
data, _ := os.ReadFile(path)
json.Unmarshal(data, &obj)
return nil
}
正确姿势是把它包进 defer 函数里,且必须在 panic 可能发生的代码**之前**注册:
- 把
defer func() { if r := recover(); r != nil { /* 处理 */ } }()放在函数开头 - 确保它在
os.ReadFile、json.Unmarshal、或任何第三方解析逻辑执行前就已注册 - 多个
defer按后进先出顺序执行,所以recover的defer要比可能触发 panic 的操作“更晚注册”才更靠前执行(但通常放最前面最安全)
recover只能捕获当前goroutine的panic,跨goroutine无效
如果你在解析文件时启了 goroutine(比如并发解析多个文件),然后在主 goroutine 的 defer 里放 recover,那完全没用——子 goroutine panic 后静默终止,主函数毫不知情。
典型误用场景:
func parseFilesConcurrently(paths []string) {
defer func() { recover() }() // ← 对子 goroutine 的 panic 完全无效
for _, p := range paths {
go func(path string) {
data, _ := os.ReadFile(path)
json.Unmarshal(data, &obj) // 这里 panic,没人 catch
}(p)
}
}
解决办法只有一个:每个可能 panic 的 goroutine 入口处,自己配 defer + recover:
- 在
go func() { ... }()内部第一行就写defer func() { if r := recover(); r != nil { log.Printf("parse %s failed: %v", path, r) } }() - 别指望“统一兜底”,Go 不提供跨 goroutine 的 panic 传播机制
- 尤其注意 HTTP handler 中启动的异步解析:goroutine 里不加 recover,一次非法 JSON 就可能导致连接句柄、数据库连接等资源泄漏
recover后无法继续执行原逻辑,副作用不可回滚
文件解析过程中若已写入临时文件、修改了全局状态、或往 channel 发了部分数据,recover 成功后这些操作不会撤销。它只让函数“不崩溃”,不等于“重来一遍”。
例如:
func parseAndSave(path string) error {
defer func() {
if r := recover(); r != nil {
log.Println("recover:", r)
}
}()
data, _ := os.ReadFile(path)
json.Unmarshal(data, &obj)
os.WriteFile("/tmp/parsed.json", data, 0644) // ← 这行执行了
panic("malformed struct") // ← 这行 panic
return nil // ← 这行永远不会执行
}
这段代码会留下一个脏的 /tmp/parsed.json,而函数返回值却是 nil(因为没显式 return)。真实项目中容易因此产生数据不一致。
建议做法:
- 把解析和写入拆成两步:先解析到内存,成功后再写磁盘
- 用临时文件 +
os.Rename做原子提交,避免中间态残留 - 不要依赖
recover实现“降级逻辑”,该用error返回的,就老实用if err != nil判断
recover返回值类型不确定,直接打印可能掩盖问题
文件解析 panic 的来源可能是:panic("invalid JSON")(string)、panic(errors.New("EOF"))(error)、甚至 runtime 抛出的 runtime.errorString(未导出类型)。直接 fmt.Println(r) 能输出,但无法区分类型,也拿不到堆栈。
更稳妥的处理方式:
- 用
fmt.Sprintf("%v", r)获取可读字符串,兼容所有类型 - 若需结构化处理(比如对某些 panic 类型做特殊日志或上报),先做类型断言:
if s, ok := r.(string); ok { ... }或if err, ok := r.(error); ok { ... } - 避免
err.(error)强断言 runtime panic,它可能 panic 再次触发崩溃 - 生产环境建议搭配
debug.PrintStack()记录完整调用栈,仅在 recover 分支中调用
真正难处理的从来不是 panic 本身,而是 panic 发生后你是否清楚哪些资源已被修改、哪些 channel 已被发送、哪些锁还握在手里——recover 不负责告诉你这些,它只负责让你的程序别立刻死掉。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











