recover 捕获 panic 后默认不输出堆栈,需手动调用 debug.stack()(推荐)或 runtime.stack() 获取;recover 必须在 defer 中且仅对当前 goroutine 有效,每个 goroutine 需独立配置;panic 值非 error 接口类型,不可直接传给 zap.error()。

Go 里 recover 捕获 panic 后,默认不输出堆栈——只拿到 panic 值,调用链就断了。必须手动调用 runtime.Stack 或 debug.Stack 才能还原现场。
recover 必须配合 defer 才生效,且仅作用于当前 goroutine
这是最常被忽略的前提。写成普通函数调用、或放在主 goroutine 的 defer 里想捕获子协程 panic,都会失败。
-
recover()在非defer函数中永远返回nil - 每个 goroutine 都要单独配一套
defer+recover,比如用safeGo(fn)封装启动逻辑 - HTTP handler 中的中间件,必须在每个请求对应的 goroutine 内做
defer,不能靠全局注册
runtime.Stack 和 debug.Stack 的关键区别
两者都用来抓堆栈,但行为和适用场景不同:
-
runtime.Stack(buf, false):只抓当前 goroutine,缓冲区大小需手动预估(如make([]byte, 4096)),超长会被截断 -
debug.Stack():返回[]byte,无需预分配 buf,适合日志记录或上报;它等价于runtime.Stack+false,但更安全 -
debug.PrintStack():直接输出到os.Stderr,若服务重定向了 stderr(如 systemd 管理的进程),可能根本看不到日志
生产环境推荐统一用 debug.Stack(),转成字符串后交给结构化日志库(如 zap.Error(err))处理。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
error 类型的堆栈不是 panic 堆栈,别混用 %+v
fmt.Printf("%+v", err) 能展开堆栈,前提是这个 err 是由 pkg/errors.Wrap 或 Go 1.20+ 的 fmt.Errorf("xxx: %w", err) 构造的。标准库 errors.New("xxx") 返回的 error 根本没存调用帧,%+v 对它完全无效。
- HTTP handler 入口用
recover捕获 panic,再用debug.Stack()打印;业务函数返回的error则应统一包装(如errors.WithStack(err)),并在日志中用%+v输出 - 别把
recover()捕获到的 panic 值直接当error传给zap.Error()——它不是实现了error接口的类型,zap 不会自动提取堆栈字段 - 如果 panic 值是
error类型(比如panic(fmt.Errorf("xxx"))),仍需用debug.Stack()单独抓,因为%+v对 panic 值本身不起作用
调试时堆栈够用,上线后得收敛敏感信息
开发阶段加 debug.Stack() 很方便,但上线后要注意两点:
- 堆栈路径可能含绝对路径或临时目录名,暴露部署结构;建议日志入库前过滤掉
/home/、/tmp/等段 -
debug.Stack()不包含局部变量值,也拿不到寄存器状态,别指望靠它还原 panic 前的变量快照;真要深度分析,得结合runtime/debug.WriteHeapDump或 Delve 调试 - 频繁调用
debug.Stack()有性能开销(尤其在热路径上),只应在异常分支中使用,不要放进循环或高频函数
真正难的不是怎么打印堆栈,而是判断该在哪一层捕获、哪一层包装、哪一层透出——panic 是信号,error 是契约,堆栈只是帮你看清契约破裂时谁签错了字。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










