目录

Go 使用 recover 捕获 panic 并打印代码堆栈日志

在 Go 开发中,panic 一旦触发且未被处理,程序就会直接崩溃退出。

这在生产环境中是极具风险的——可能导致服务中断、数据丢失等严重问题。

recover 作为 Go 提供的“ panic 救援”机制,能帮助我们捕获 panic 并恢复程序运行,再配合堆栈日志打印,就能快速定位触发 panic 的代码位置。

今天就带大家一步步掌握这个核心技能。

一、panic、recover 与堆栈日志的关系

先理清三个关键概念的逻辑,避免后续使用时踩坑:

  • panic:Go 中的“运行时恐慌”,通常由程序逻辑错误触发(比如数组越界、空指针引用),触发后会终止当前协程的执行并向上传播。

  • recover:专门用于捕获 panic 的内置函数,只有在 defer 语句中调用才有效,能阻止 panic 继续传播并返回 panic 的错误信息。

  • 堆栈日志:记录 panic 触发时的函数调用链路,包括文件名、行号和函数名,是定位问题的“核心线索”。

简单说:defer 负责“延迟执行” recoverrecover 负责“捕获” 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.Callerskip 参数(或 runtime.Stack 的缓冲区大小)。

skip 值太小会打印过多底层函数(比如 runtime 包的函数),太大则会跳过业务代码——一般 skip=23 比较合适,可根据实际调用链路调整;

② 缓冲区太小会导致堆栈信息被截断,建议设置 1024*1024(1MB)以上的缓冲区。

Q3. 可以在子协程中用 recover 捕获主协程的 panic 吗?

不可以。

panicrecover 是“协程隔离”的——子协程的 recover 只能捕获当前子协程的 panic,主协程的 panic 会直接导致程序退出,除非主协程自己用 recover 捕获。

如果要管理子协程的 panic,需要在每个子协程内部单独部署 defer + recover

总结

Go 中的 recover + defer 是处理 panic 的“黄金组合”,而堆栈日志则是定位问题的“关键证据”。总结如下:

  • 必须在 defer 中调用 recover,且 defer 要定义在可能触发 panic 的代码之前;

  • 借助 runtime.Callerruntime.Stack 获取堆栈信息,合理设置 skip 参数和缓冲区大小;

  • 生产环境中建议封装独立的日志工具函数,支持日志格式化和文件写入;

  • panic 不是“错误”的替代品,仅用于处理不可恢复的运行时错误,普通业务错误建议用返回值处理。

掌握这个技巧后,能大幅提升 Go 程序的稳定性,即使出现 panic 也能“优雅降级”并快速定位问题,避免服务直接崩溃造成损失。

版权声明

未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!

本文原文链接: https://fiveyoboy.com/articles/go-recover-stack-log/

备用原文链接: https://blog.fiveyoboy.com/articles/go-recover-stack-log/