recover() 无法获取完整堆栈,因其仅返回 panic 值;需在 defer 中配合 runtime.caller(1) 和 runtime.stack(buf, false) 主动捕获位置与精简堆栈,并过滤非业务 panic。

panic 日志为什么不能直接用 recover() 拿到完整堆栈?
Go 的 recover() 只能捕获 panic 时传入的 interface{} 值(比如 panic("oops") 中的字符串),但原始堆栈信息、goroutine 状态、调用链深度等,早被运行时丢弃了——recover() 本身不返回堆栈。
真正能拿到完整堆栈的,只有 runtime.Stack() 或 debug.PrintStack(),但它们必须在 panic 发生后、程序崩溃前的同一 goroutine 中主动调用,且需配合 defer + recover() 使用。
- 漏掉
defer就彻底没机会捕获 - 在子 goroutine 中 panic,主 goroutine 的
recover()无效 -
runtime.Stack(buf, false)只打当前 goroutine;true才打全部,但会显著拖慢 recovery 过程(尤其高并发场景)
如何用 runtime.Caller() 和 runtime.FuncForPC() 补全 panic 上下文?
单纯靠 panic("msg") 留下的信息太单薄。结构化日志至少要含:文件名、行号、函数名、panic 原因、时间戳。这些得靠运行时反射手动拼。
关键不是“能不能”,而是“在哪一刻取”:必须在 recover() 后立刻调用 runtime.Caller(1)(参数 1 表示跳过当前函数,定位到 panic 触发点)。
立即学习“go语言免费学习笔记(深入)”;
-
runtime.Caller(0)返回的是recover()所在函数的位置,没用 -
runtime.FuncForPC(pc).Name()能拿到函数全名(如"main.handleRequest"),但若函数是闭包或内联,可能返回"?" - 注意:
runtime.Caller()返回的pc在某些 build mode(如-ldflags="-s -w")下可能失效,导致文件/行号为空
func capturePanic() map[string]interface{} {
if r := recover(); r != nil {
pc, file, line, ok := runtime.Caller(1)
fn := "unknown"
if ok && pc != 0 {
if f := runtime.FuncForPC(pc); f != nil {
fn = f.Name()
}
}
return map[string]interface{}{
"panic_value": r,
"file": file,
"line": line,
"function": fn,
"timestamp": time.Now().UTC().Format(time.RFC3339),
}
}
return nil
}
JSON 结构化日志里要不要存原始堆栈字符串?
要,但得控制体积和性能。原始堆栈(debug.Stack() 返回的 []byte)包含所有 goroutine 状态,动辄几百 KB,直接塞进 JSON 容易撑爆日志系统或触发采样截断。
更实用的做法是:只存「触发 panic 的 goroutine」的精简堆栈(用 runtime.Stack(buf, false)),并限制长度(比如 4KB);同时把完整堆栈异步写入独立 debug 文件或上报通道。
- 别用
string(debug.Stack())直接赋值给 JSON 字段——UTF-8 编码可能含不可见控制符,破坏日志解析 - 建议先用
bytes.TrimSpace()清理首尾空白,再用strings.ReplaceAll(s, " ", "\n")转义换行(否则 JSON 不合法) - 如果服务启用了
GODEBUG=asyncpreemptoff=1,runtime.Stack()可能阻塞更久,需设超时或降级为仅记录位置
为什么自定义 panic 解析器上线后日志量暴增?
因为没过滤非业务 panic。Go 标准库、中间件、HTTP server 内部常有受控 panic(比如 http: panic serving ... 被 http.Server 自动 recover),你一视同仁地全解析,就等于把底层错误也当业务异常上报了。
- 检查
recover()得到的值是否为error类型,优先处理实现了Error()方法的 panic 值 - 对已知安全 panic(如
http.ErrAbortHandler)显式忽略,避免污染日志 - 加计数器 + 采样率控制(例如每分钟最多上报 5 条相同 panic 类型),否则一个循环 panic 会瞬间打满磁盘
最常被忽略的一点:log.Panic 和 log.Fatal 本身就会调用 os.Exit(1),根本不会走到你的 recover() 逻辑里——别指望它们被结构化解析。










