go 1.22+ 应使用 runtime.setpanichandler 捕获所有未恢复 panic;此前版本只能通过主 goroutine 中 recover + signal 处理显式 panic,无法捕获协程中未 recover 的 panic。

panic发生时程序已崩溃,无法用defer恢复
Go的panic会终止当前goroutine,若未被recover捕获,将导致整个程序退出。但主goroutine的未捕获panic仍有机会在退出前记录堆栈——关键在于runtime.SetPanicHandler(Go 1.22+)或传统recover + os/signal组合的局限性要分清:前者是唯一能稳定捕获**所有未恢复panic**的机制,后者只能捕获显式panic调用点,且对协程中未recover的panic无效。
如果你用的是Go SetPanicHandler——它不存在,直接报undefined: runtime.SetPanicHandler错误。
Go 1.22+:用runtime.SetPanicHandler注册全局钩子
这是目前最可靠的方式,它会在panic传播到goroutine栈顶、程序终止前被调用,且保证只执行一次(即使多个panic并发发生)。
- 必须在
main函数开头尽早注册,否则可能错过早期panic - handler函数接收
*runtime.PanicError,包含Recovered() bool(始终为false)、Stack() []byte(原始堆栈)、Value() interface{}(panic参数) - 不要在handler里再
panic或阻塞,否则程序可能卡死或日志丢失
func main() {
runtime.SetPanicHandler(func(p *runtime.PanicError) {
log.Printf("GLOBAL PANIC: %v\n%s", p.Value(), p.Stack())
// 可选:写入文件、上报监控、触发告警
})
// ... 其他初始化
}
Go recover兜底
这个方案有明显缺陷:它**完全捕获不到其他goroutine中的未recover panic**,仅对main goroutine有效。很多线上panic实际发生在HTTP handler、定时任务等新启goroutine中,这种方案会静默丢弃。
- 必须把
defer + recover包在main函数最外层,不能放在子函数里 -
recover()只对当前goroutine生效,且必须在panic后、goroutine结束前调用 - 无法获取完整goroutine栈,
debug.PrintStack()输出的是当前栈(即recover位置),不是panic源头
func main() {
defer func() {
if r := recover(); r != nil {
buf := make([]byte, 4096)
n := runtime.Stack(buf, false)
log.Printf("MAIN GOROUTINE PANIC: %v\n%s", r, buf[:n])
}
}()
// ... 启动服务
}
日志内容必须包含panic值和原始堆栈,不能只打debug.PrintStack()
很多人误以为debug.PrintStack()能拿到panic现场信息,其实它只打印当前调用栈,和panic无关。真正需要的是panic时的值(比如"index out of range")和触发panic那一行的完整调用链。
- Go 1.22+:用
p.Value()和p.Stack(),二者缺一不可 - 旧版本兜底方案:
recover()返回值就是panic值,但堆栈需用runtime/debug.Stack()(注意不是PrintStack) - 避免用
fmt.Sprintf("%+v", err)替代堆栈,它不展开goroutine帧,看不出在哪一行panic
容易忽略的一点:如果panic值是自定义error且实现了Error()方法,p.Value()返回的就是该方法结果,不是原始panic对象——但通常这正是你想要的日志内容。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











