
本文介绍如何捕获 go 程序中的 panic 堆栈信息,自动添加 iso 8601 格式时间戳,并完整写入日志文件,避免默认 panic 输出丢失上下文或缺乏可追溯性。
本文介绍如何捕获 go 程序中的 panic 堆栈信息,自动添加 iso 8601 格式时间戳,并完整写入日志文件,避免默认 panic 输出丢失上下文或缺乏可追溯性。
Go 默认的 panic 输出直接打印到 stderr,不带时间戳,也不易持久化。要实现带时间戳的 panic 日志并写入文件,核心思路是:拦截 panic → 捕获堆栈 → 添加时间前缀 → 写入文件。这无法通过简单设置 log 包标志(如 log.Ltime)完成,因为 panic 的原始堆栈由运行时自动生成,不经过 log.Logger。正确做法是结合 recover、debug.PrintStack(或 runtime.Stack)与自定义格式化输出。
以下是推荐的生产级实现方案:
✅ 步骤一:注册全局 panic 恢复钩子
在 main() 开头使用 defer + recover 捕获所有未处理 panic,并将带时间戳的完整堆栈写入文件:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
package main
import (
"fmt"
"log"
"os"
"runtime/debug"
"time"
)
func init() {
// 创建带时间戳的日志文件(按需可每日轮转)
logFile, err := os.OpenFile("panic.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
log.Fatalf("无法打开 panic 日志文件: %v", err)
}
log.SetOutput(logFile)
log.SetFlags(0) // 关闭 log 自带时间戳,我们手动添加
}
func main() {
defer func() {
if r := recover(); r != nil {
// 获取当前时间(ISO 8601 格式,含毫秒和 Z 时区标识)
timestamp := time.Now().UTC().Format("2006-01-02T15:04:05.000Z")
// 打印时间戳 + panic 值
log.Printf("%s - panic: %v", timestamp, r)
// 打印完整堆栈(等效于默认 panic 输出)
log.Print(string(debug.Stack()))
}
}()
// 示例 panic 触发点
panic("示例错误:用户操作失败")
}✅ 输出效果(完全匹配预期):
2024-05-20T08:32:15.789Z - panic: 示例错误:用户操作失败 goroutine 1 [running]: runtime/debug.Stack() .../debug/stack.go:24 +0x7a main.main.func1() /tmp/main.go:28 +0x3c panic({0x...}) .../panic.go:884 +0x212 main.main() /tmp/main.go:35 +0x45
⚠️ 注意事项与最佳实践
-
不要在
recover中再次 panic:避免嵌套 panic 导致程序崩溃且日志丢失。 -
确保日志文件可写:生产环境建议使用绝对路径、检查权限,并考虑日志轮转(如借助
lumberjack)。 -
时间精度:
time.Now().UTC().Format("2006-01-02T15:04:05.000Z")提供毫秒级精度,符合 RFC 3339;如需更高精度(微秒),可用.Format("2006-01-02T15:04:05.000000Z")。 -
debug.Stack()vsruntime.Stack():前者返回[]byte更简洁;后者需传入[]byte缓冲区,适合内存敏感场景。 - 避免阻塞主线程:若日志写入耗时(如网络存储),建议异步写入(如通过 channel + goroutine),但需注意 panic 时 goroutine 可能被终止,因此同步写入更可靠。
✅ 进阶:封装为可复用函数
func SetupPanicLogger(filename string) {
f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
log.Fatalf("SetupPanicLogger: %v", err)
}
log.SetOutput(f)
go func() {
defer func() {
if r := recover(); r != nil {
ts := time.Now().UTC().Format("2006-01-02T15:04:05.000Z")
fmt.Fprintf(f, "%s - panic: %v\n%s\n", ts, r, debug.Stack())
f.Sync() // 强制刷盘,防止崩溃丢日志
}
}()
}()
}调用 SetupPanicLogger("app_panic.log") 即可一键启用。
通过以上方式,你不仅能获得带精确时间戳的 panic 日志,还能统一格式、增强可观测性,为故障排查提供关键时间线索。

















