recover必须在defer中调用且仅捕获同goroutine的panic;每个连接goroutine需独立defer recover,main中recover无效;panic仅用于不可恢复状态,io错误应走error分支;recover后必须显式conn.close()防资源泄漏。

recover必须在defer里调用,且只能捕获当前goroutine的panic
Go没有全局异常处理器,recover() 只对同 goroutine 中、由 defer 延迟执行的函数有效。你在 TCP 服务中为每个连接启一个 goroutine(比如 go handleConnection(conn)),那每个连接的崩溃就必须在它自己的 goroutine 里用 defer recover() 挡住,否则 panic 会直接杀掉整个进程。
常见错误是只在 main 函数里写一次 defer recover()——这完全没用,因为 main 的 defer 只能捕获 main 自己 panic,拦不住子 goroutine 的崩溃。
- 每个连接处理函数开头必须加
defer func() { if r := recover(); r != nil { log.Printf("conn %v panic: %v", conn.RemoteAddr(), r) } }() - 不要把 recover 提到上层共用函数里——goroutine 隔离,
recover()跨不了 goroutine - recover 返回值类型是
interface{},不是error;想转成结构化错误得自己判断和封装
panic不该用于网络IO错误,但可用来兜底不可恢复状态
TCP 中转站里,read 或 write 返回 io.EOF、net.ErrClosed、超时错误,这些都该走正常 if err != nil 分支处理,而不是 panic。panic 只该留给真正“程序逻辑崩了”的场景:比如配置加载失败后仍继续运行、解包协议时发现非法长度导致内存越界风险、或关键状态机进入不可能状态。
- HTTP 服务里误把
http.Error()写成panic("404")是典型滥用——它会让连接 goroutine 崩溃,但错误本可优雅返回 - 中转逻辑中,如果从上游读到 10MB 数据却只分配了 1KB 缓冲区,这种缓冲区溢出风险可以 panic,但得配合 recover 记录堆栈并关闭连接
- 用
fmt.Errorf("%w", err)包装原始 error,比直接 panic 更利于日志追踪和重试决策
带返回值的recover封装要小心返回nil还是具体错误
很多人想写个统一的 safeRun(func() error) 来包裹连接处理逻辑,让它返回 error 而不是让 goroutine 崩溃。但要注意:recover() 本身不返回 error,你得自己构造。
下面这个模式可行:
func safeHandle(conn net.Conn) error {
defer func() {
if r := recover(); r != nil {
log.Printf("panic on %v: %v", conn.RemoteAddr(), r)
}
}()
if err := handleConnection(conn); err != nil {
return err
}
return nil
}
但问题在于:如果 handleConnection 内部 panic,safeHandle 仍返回 nil——你失去了“这次连接是否成功完成”的信号。更稳妥的做法是让 recover 分支也返回一个带上下文的 error:
- 定义一个
ConnPanicError类型,实现Error()方法,包含 remote addr 和 panic 值 - 在 defer 里用闭包变量捕获 panic 结果,最后统一 return
- 不要在 recover 里尝试重连或重发数据——panic 表示当前连接上下文已不可信,直接关连接最安全
中转站里 recover 后必须显式关闭连接
recover 拦住 panic 只是防止进程退出,不代表连接还能用。TCP 连接对象(net.Conn)在 panic 发生时很可能处于半读/半写状态,底层 socket 可能已损坏或资源泄漏。这时候不关连接,会导致文件描述符泄露,最终 accept 失败或 too many open files。
- 所有 recover 分支末尾必须加
conn.Close(),哪怕之前已经调过 defer —— defer 在 panic 后仍会执行,但多关一次无害,少关一次致命 - 别依赖
defer conn.Close()就以为万事大吉:如果 panic 发生在 defer 注册前(比如 listener.Accept() 后、conn 赋值前),defer 根本不会注册 - 用
net.Conn.SetDeadline配合 recover,避免某条连接卡死拖垮整个服务











