Go 使用 recover 捕获 panic 并打印代码堆栈日志
在 Go 开发中,panic 一旦触发且未被处理,程序就会直接崩溃退出。
这在生产环境中是极具风险的——可能导致服务中断、数据丢失等严重问题。
而 recover 作为 Go 提供的“ panic 救援”机制,能帮助我们捕获 panic 并恢复程序运行,再配合堆栈日志打印,就能快速定位触发 panic 的代码位置。
今天就带大家一步步掌握这个核心技能。
一、panic、recover 与堆栈日志的关系
先理清三个关键概念的逻辑,避免后续使用时踩坑:
-
panic:Go 中的“运行时恐慌”,通常由程序逻辑错误触发(比如数组越界、空指针引用),触发后会终止当前协程的执行并向上传播。
-
recover:专门用于捕获
panic的内置函数,只有在defer语句中调用才有效,能阻止panic继续传播并返回panic的错误信息。 -
堆栈日志:记录
panic触发时的函数调用链路,包括文件名、行号和函数名,是定位问题的“核心线索”。
简单说:defer 负责“延迟执行” recover,recover 负责“捕获” panic,而堆栈日志负责“说清” panic 在哪发生。
二、基础版:捕获 panic 并打印简单堆栈
先实现最核心的功能:捕获 panic 并打印基础堆栈。
代码如下,关键步骤已加注释:
package main
import (
"fmt"
"runtime"
)
// 模拟一个会触发 panic 的函数
func riskyOperation() {
// 故意制造数组越界错误
arr := [3]int{1, 2, 3}
fmt.Println(arr[5]) // 数组下标越界,触发 panic
}
// 封装错误恢复和日志打印的函数
func safeExecute(fn func()) {
// defer 延迟执行,确保在 panic 时也能调用
defer func() {
// 调用 recover 捕获 panic
if err := recover(); err != nil {
fmt.Printf("捕获到 panic:%v\n", err)
fmt.Println("代码堆栈日志:")
// 打印堆栈日志,skip=2 跳过当前 defer 函数和 safeExecute 函数,显示实际调用链路
stackTrace := make([]byte, 1024*1024) // 分配 1MB 缓冲区存堆栈信息
n := runtime.Stack(stackTrace, false) // 获取堆栈信息,false 表示不包含当前 goroutine 信息
fmt.Printf("%s\n", stackTrace[:n]) // 打印堆栈信息
}
}()
// 执行可能触发 panic 的函数
fn()
}
func main() {
fmt.Println("程序开始执行")
// 安全执行风险函数
safeExecute(riskyOperation)
fmt.Println("程序正常退出") // 若 panic 被捕获,这里会正常执行
}运行结果解析:程序会先打印“程序开始执行”,然后 riskyOperation 触发数组越界 panic,此时 defer 中的 recover 会捕获错误,打印出 panic 信息和堆栈日志,最后程序正常打印“程序正常退出”,不会崩溃。
三、进阶版:封装堆栈日志工具函数
基础版的堆栈日志比较粗糙,生产环境中我们需要更清晰的格式,还可能需要将日志写入文件。
下面优化代码,封装一个可复用的堆栈日志工具函数:
package main
import (
"fmt"
"os"
"runtime"
"time"
)
// 获取格式化的堆栈日志
func getStackTrace(skip int) string {
var stack []string
// 循环获取调用栈信息,直到没有更多帧
for i := skip; ; i++ {
// 获取函数名、文件名和行号
pc, file, line, ok := runtime.Caller(i)
if !ok {
break
}
// 获取函数名(去掉包名前缀)
funcName := runtime.FuncForPC(pc).Name()
// 格式化每一行堆栈信息
stackLine := fmt.Sprintf("[%s:%d] %s", file, line, funcName)
stack = append(stack, stackLine)
}
// 拼接所有堆栈行
result := ""
for _, line := range stack {
result += line + "\n"
}
return result
}
// 模拟生产环境中的风险函数(比如数据库操作)
func dbQuery() {
// 模拟数据库连接失败触发 panic
panic("数据库连接超时:无法连接到 MySQL 服务")
}
// 生产级别的安全执行函数,支持日志写入文件
func productionSafeExecute(fn func(), logPath string) {
defer func() {
if err := recover(); err != nil {
// 构造日志内容:包含时间、错误信息、堆栈
logContent := fmt.Sprintf(
"【%s】捕获到 panic:%v\n堆栈日志:\n%s",
time.Now().Format("2006-01-02 15:04:05"),
err,
getStackTrace(2), // skip=2 跳过 defer 和 productionSafeExecute
)
// 打印到控制台
fmt.Println(logContent)
// 写入日志文件
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
fmt.Printf("写入日志文件失败:%v\n", err)
return
}
defer file.Close()
file.WriteString(logContent + "\n\n") // 写入文件并换行分隔
}
}()
fn()
}
func main() {
fmt.Println("生产环境程序启动")
// 安全执行数据库查询函数,日志写入 ./panic.log
productionSafeExecute(dbQuery, "./panic.log")
fmt.Println("服务继续运行中...")
}进阶版优势:
① 封装 getStackTrace 函数,让堆栈日志显示“文件名:行号 函数名”的清晰格式;
② 加入时间戳,方便追溯问题发生时间;
③ 支持将日志写入文件,符合生产环境日志归档需求;
④ 模拟真实业务场景(数据库连接),更具实用性。
常见问题
Q1. recover 为什么捕获不到 panic?
最常见的原因有两个:
① recover 没有在 defer 语句中调用——只有 defer 能保证在 panic 触发后、函数退出前执行;
② defer 语句定义在 panic 之后——比如先执行 riskyOperation() 再写 defer,此时 panic 触发时 defer 还没注册,自然无法捕获。
Q2. 堆栈日志打印不完整或看不到业务代码?
问题出在 runtime.Caller 的 skip 参数(或 runtime.Stack 的缓冲区大小)。
① skip 值太小会打印过多底层函数(比如 runtime 包的函数),太大则会跳过业务代码——一般 skip=2 或 3 比较合适,可根据实际调用链路调整;
② 缓冲区太小会导致堆栈信息被截断,建议设置 1024*1024(1MB)以上的缓冲区。
Q3. 可以在子协程中用 recover 捕获主协程的 panic 吗?
不可以。
panic 和 recover 是“协程隔离”的——子协程的 recover 只能捕获当前子协程的 panic,主协程的 panic 会直接导致程序退出,除非主协程自己用 recover 捕获。
如果要管理子协程的 panic,需要在每个子协程内部单独部署 defer + recover。
总结
Go 中的 recover + defer 是处理 panic 的“黄金组合”,而堆栈日志则是定位问题的“关键证据”。总结如下:
-
必须在
defer中调用recover,且defer要定义在可能触发panic的代码之前; -
借助
runtime.Caller或runtime.Stack获取堆栈信息,合理设置skip参数和缓冲区大小; -
生产环境中建议封装独立的日志工具函数,支持日志格式化和文件写入;
-
panic不是“错误”的替代品,仅用于处理不可恢复的运行时错误,普通业务错误建议用返回值处理。
掌握这个技巧后,能大幅提升 Go 程序的稳定性,即使出现 panic 也能“优雅降级”并快速定位问题,避免服务直接崩溃造成损失。
版权声明
未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!
本文原文链接: https://fiveyoboy.com/articles/go-recover-stack-log/
备用原文链接: https://blog.fiveyoboy.com/articles/go-recover-stack-log/