如何捕获并自定义 Go 程序的 panic 输出

本文介绍如何在 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 服务的关键实践之一。