recover必须在defer中调用且仅对当前goroutine生效;网络协议解析易因越界、空指针等触发panic,故每个可能panic的函数(如ParsePacket)须内置defer+recover防护,不可依赖外层统一recover。

recover必须在defer里调用,且只能捕获本goroutine panic
网络协议解析(比如自定义二进制协议、JSON/XML反序列化、HTTP header解析)一旦遇到畸形报文,极易触发运行时 panic:切片越界、空指针解引用、json.Unmarshal 传入 nil 指针、unsafe.Pointer 转换越界等。但 recover 不是“全局开关”,它只对**当前 goroutine** 生效——HTTP handler 启动一个新 goroutine 去解析,就得在那个 goroutine 内部自己加 defer + recover。
常见错误包括:
- 在 main 函数或中间件里写一次
defer func() { recover() }(),以为能兜住所有 handler 的 panic —— 实际无效 - 把
recover()放在普通函数体里(非 defer 匿名函数),永远返回nil - 在解析逻辑之后才注册
defer,panic 已发生,来不及捕获
解析函数必须自己包一层 defer+recover,不能依赖外层
假设你写了一个 ParsePacket(buf []byte) (Packet, error),它内部可能调用 binary.Read、json.Unmarshal 或手动索引 buf[4:8]。这类函数绝不能裸奔,必须自带防护:
func ParsePacket(buf []byte) (Packet, error) {
var pkt Packet
// 必须提前注册,且在 defer 中调用 recover
defer func() {
if r := recover(); r != nil {
// 注意:这里不能继续用 buf 或 pkt,状态可能已损坏
log.Printf("ParsePacket panic: %v (buf len=%d)", r, len(buf))
}
}()
// 解析逻辑(可能 panic)
if len(buf) <p>关键点:</p>
-
defer必须在任何可能 panic 的代码之前声明 - 不要在
recover后尝试继续构造pkt或返回部分结果 —— 切片越界后buf可能已处于未定义状态 - 推荐模式是:
if r := recover(); r != nil { log...; return Packet{}, errors.New("parse failed") }
recover 后不能信任原始数据或中间状态
这是最容易被忽略的危险点。recover 能让你函数返回,但不等于“恢复了安全状态”。例如:
- 如果 panic 来自
buf[100](越界),说明buf长度不足,但 recover 后你仍可能误用buf[:10]—— 这没问题;但若 panic 来自并发写 map,recover 后再读该 map 会再次 panic - 如果解析中已修改了某个全局缓存或共享结构体字段,recover 并不会回滚这些副作用
-
json.Unmarshalpanic(如传入未初始化的 struct 字段)后,目标 struct 可能处于半初始化状态,不应直接返回
所以实际工程中,更稳妥的做法是:
- 在
defer中只做日志和清理(如关闭临时 buffer、释放 pool 对象) - 业务逻辑返回前,统一设为零值:
return Packet{}, fmt.Errorf("malformed packet") - 避免在 recover 分支里调用任何可能再次 panic 的操作(如
close(ch)、向已破坏的 map 写入、访问pkt.Header.Name)
HTTP handler 场景下,每个请求都需独立防护
net/http 默认不 recover,Gin/Echo 等框架虽内置 recover 中间件,但如果你自己写了底层解析逻辑(比如从 req.Body 读 raw bytes 再 hand-roll 解析),框架的中间件捕获不到你 goroutine 内部的 panic。
正确做法是:在 handler 内部、真正调用解析函数前就加 defer:
func handlePacket(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
http.Error(w, "bad request", http.StatusBadRequest)
log.Printf("handler panic: %v", r)
}
}()
buf, _ := io.ReadAll(r.Body)
pkt, err := ParsePacket(buf) // 这里可能 panic
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// ...
}
注意:io.ReadAll 本身也可能 panic(极少见,但自定义 Read 实现可能),所以 defer 必须包住整个处理链起点。别指望“统一中间件”能替代具体解析函数内的防护。
复杂协议解析的真正难点不在 recover 语法,而在于:panic 发生时,你根本不知道哪些内存、哪些字段、哪些 channel 已被污染。防御的核心不是“抓住 panic”,而是让 panic 尽早发生、尽早退出、绝不复用可疑状态。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











