recover()在goroutine中默认不生效,因为panic仅终止当前goroutine且不传播,recover必须在同goroutine中、panic前注册的defer函数内直接调用才能捕获;跨goroutine或非defer调用均无效。

为什么 recover() 在 goroutine 中默认不生效
Go 没有传统意义的“异常”,panic() 会终止当前 goroutine,但不会自动传播到启动它的父 goroutine。如果你在长连接消费者里直接用 go func() { ... panic() ... }() 启动任务,recover() 必须出现在同一个 goroutine 的 defer 链中,否则完全捕获不到。
常见错误是把 defer recover() 放在主函数或上层函数里,结果 panic 后程序直接退出——因为出问题的 goroutine 已经死了,没人救它。
-
recover()只对同 goroutine 内、且在 panic 发生前已注册的 defer 生效 - 长连接循环(如
for { conn.Read() })必须包裹在单个 goroutine 内部的 defer-recover 结构中 - 不要依赖外层 goroutine 的 defer 去“兜底”内层 panic
如何正确包装一个 TCP 长连接消费者
核心是让每次连接生命周期(从建立到断开)都在一个独立 goroutine 中闭环处理,且 panic 恢复逻辑紧贴业务读写循环。
func runConsumer(conn net.Conn) {
defer func() {
if r := recover(); r != nil {
log.Printf("consumer panicked: %v, reconnecting...", r)
}
conn.Close()
}()
for {
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
log.Printf("read error: %v", err)
return // 主动退出,触发 defer
}
// 处理消息:这里任何 panic 都会被 recover 捕获
handleMessage(buf[:n])
}
}
- 每个
conn对应一个runConsumer()调用,不复用 goroutine -
handleMessage()内部若调用第三方库或做复杂解码,可能 panic,必须在该 goroutine 内 recover - 不要在
recover()后继续for循环——连接已不可靠,应退出并由上层重连
重连逻辑放在哪里才不算“自愈失败”
恢复 panic 只是第一步;真正“自愈”体现在连接中断后能否静默重建。重连不能放在 recover() 分支里直接循环 dial,否则会掩盖真实失败原因(比如服务端永久下线),还可能打爆远端。
- 重连决策应由上层控制,例如用带退避的
for { conn, err := net.Dial(...); if err == nil { go runConsumer(conn) }; time.Sleep(backoff()) } -
runConsumer()本身只负责单次连接生命周期内的 panic 捕获和清理,不主动重连 - 如果连接刚建立就 panic(如 TLS 握手后立即 decode 失败),需检查是否误将协议解析逻辑塞进连接建立流程
容易被忽略的资源泄漏点
recover 能止住 panic,但挡不住资源没释放。最典型的是:conn 没 close、buffer 没复用、time.Timer 没 Stop。
- 务必在 defer 中显式调用
conn.Close(),哪怕 panic 发生在Read()前(比如初始化时) - 避免在
handleMessage()中启动无管控 goroutine(如go sendAck()),它们可能持有 conn 引用导致无法回收 - 使用
sync.Pool管理频繁分配的 buffer,否则 recover 频繁时 GC 压力陡增
自愈不是让 panic 变得无害,而是把不可控崩溃,变成可控的连接生命周期管理问题。关键不在“抓得住”,而在“放得清”。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











