go无全局panic捕获,rpc中panic逃逸常见于子goroutine;需三层recover:main入口、http/grpc拦截器最外层、所有go调用封装safego;日志必带stack trace和trace_id。

Go 没有全局 panic 捕获机制,RPC 场景下 panic 逃逸是常态——尤其在子 goroutine、gRPC 拦截器链、第三方库回调中。靠 recover() 包一层 handler 就完事,基本等于没防。
gRPC Unary/Stream 拦截器里 recover 失效的真相
你写了 grpc.UnaryInterceptor,并在函数开头加了 defer recover(),但 panic 还是炸了进程?问题不在写法,而在位置:如果 panic 发生在 handler(ctx, req) 内部的子 goroutine(比如异步日志、DB 查询回调、定时清理),拦截器里的 recover() 根本看不到它。
- gRPC 拦截器运行在主请求 goroutine,
recover()只对当前 goroutine 有效 -
handler是你传进来的业务函数,它内部起的go fn()属于新 goroutine,必须各自带defer recover() - 常见踩坑:在 Auth 拦截器里查 Redis,用
go redisClient.Get(...)异步取值 → panic 后整个服务挂掉
必须分层加 recover,不能只靠中间件
所谓“兜底”,其实是三道防线同时生效,缺一不可:
-
main goroutine:在
main()开头加defer func() { recover() }(),捕获 init 阶段或启动失败的 panic -
HTTP/gRPC 入口层:用框架自带 recovery(如
gin.Recovery()或grpc.UnaryInterceptor中的 recover),覆盖主请求流程 -
所有显式
go调用点:禁止裸写go doSomething(),统一走封装函数,例如:func safeGo(f func()) { go func() { defer func() { if r := recover(); r != nil { log.Printf("panic in goroutine: %v\n%v", r, debug.Stack()) } }() f() }() }
拦截器链中 Recovery 必须放在最外层
如果你用了多个 gRPC 拦截器(比如 Tracing → Auth → Logging → Recovery),顺序错了就等于没加:
-
Recovery放在最外层(即第一个参数传给grpc.NewServer()),才能捕获 Tracing 或 Auth 里发生的nil pointer dereference - 如果 Recovery 在内层,Auth 拦截器里
md["authorization"][0]panic,会直接触发进程退出,os.Exit(1)都来不及打日志 - Tracing 必须早于 Auth:因为 Auth 错误日志要输出
trace_id,而 trace 上下文得先从 Metadata 解析出来
panic 日志里不打 stack trace = 白记
很多团队只记录 log.Printf("panic: %v", r),结果线上出问题只能看到 panic: runtime error: invalid memory address,完全没法定位。
- 必须用
debug.Stack()或runtime/debug.PrintStack()补全调用栈 - panic 日志要带上
trace_id(从ctx里取),否则在分布式日志系统里找不到上下文 - 别依赖
debug.SetPanicOnFault(true):它只对非法内存访问信号生效,对panic("xxx")、map 写 nil、slice 越界等 99% 的业务 panic 完全无效
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











