
本文介绍如何在 go 中精确捕获 panic 时的堆栈信息(而非依赖 stderr 重定向),利用 `runtime.stack` 获取结构化、可编程处理的 panic 堆栈快照,并结合 `recover` 实现优雅错误捕获与日志增强。
Go 默认在 panic 发生时将完整的 goroutine 堆栈信息输出到 stderr 并终止程序,但这种行为难以定制——例如无法区分 panic 日志与其他错误日志,也无法在退出前做上报、采样或格式化。幸运的是,Go 提供了底层机制,让我们能主动捕获 panic 的原始堆栈数据,而非被动监听标准错误流。
核心方案是组合使用 recover() 和 runtime.Stack():
- recover() 捕获 panic 的原始值(如 panic("boom") 中的 "boom");
- runtime.Stack(buf []byte, all bool) 将当前或所有 goroutine 的堆栈写入字节切片,返回实际写入长度;
- 通过传入 true 作为第二个参数,可获取全部 goroutine 的堆栈快照(类似默认 panic 输出);传入 false 则仅获取当前 goroutine。
以下是一个完整示例,演示如何在 defer 中安全捕获 panic 并提取结构化堆栈:
package main
import (
"fmt"
"runtime"
"strings"
)
func main() {
defer func() {
if r := recover(); r != nil {
// 获取 panic 值
errMsg := fmt.Sprintf("%v", r)
// 获取所有 goroutine 的堆栈(注意:buf 需足够大)
buf := make([]byte, 1024*1024) // 1MB 缓冲区,避免截断
n := runtime.Stack(buf, true)
stack := string(buf[:n])
// ✅ 此时 errMsg 和 stack 均可自由处理:
// - 写入结构化日志(如 JSON)
// - 发送到监控系统(如 Sentry、Prometheus Alertmanager)
// - 过滤敏感信息后再落盘
fmt.Printf("PANIC CAUGHT:\n%s\nSTACK TRACE:\n%s\n", errMsg, stack)
}
}()
// 触发 panic
panic("something went wrong")
}⚠️ 注意事项:
- runtime.Stack 返回的堆栈是纯文本快照,不包含 panic 发生位置的源码行号(除非编译时保留调试信息,且运行环境支持)。若需精确定位,建议配合 -gcflags="all=-l"(禁用内联)和符号表使用。
- 缓冲区大小需预估充足(如上例使用 1MB),否则堆栈会被截断——可通过循环扩容或先调用 runtime.Stack(nil, true) 获取所需长度(Go 1.18+ 支持)。
- runtime.Stack(_, true) 开销较大(遍历所有 goroutine),仅应在 panic 处理路径中使用,切勿在高频逻辑中调用。
- 若只需当前 goroutine 堆栈(更轻量),将 true 改为 false 即可,适用于简单错误诊断场景。
总结:通过 recover + runtime.Stack 组合,你完全掌控 panic 输出的生成与流向,摆脱对 stderr 重定向的依赖,实现日志隔离、错误归因、可观测性增强等生产级需求。这是构建健壮 Go 服务的关键实践之一。








