gin.RecoveryWithWriter本身不支持日志轮转,需配合lumberjack.Logger(设MaxSize、MaxBackups、MaxAge等)与io.MultiWriter实现panic日志按大小和日期轮转写入。

gin.RecoveryWithWriter 不能直接支持日志轮转
直接传入一个 os.File 句柄给 gin.RecoveryWithWriter() 是最简方式,但它不带轮转能力——文件会越写越大,且无法按日期或大小切分。轮转必须由外部日志库或手动封装的 io.Writer 实现,gin.RecoveryWithWriter 本身只负责把 panic 堆栈写进去,不做任何格式控制或切割逻辑。
用 lumberjack + io.MultiWriter 实现 panic 日志轮转写入
推荐组合:用 lumberjack.Logger(官方维护、轻量、无依赖)做轮转 writer,再通过 io.MultiWriter 同时输出到文件和标准错误(便于本地调试)。关键点不是“替换 Recovery”,而是“换掉它写的 target”。
-
lumberjack.Logger需显式设置MaxSize(单位 MB)、MaxBackups、MaxAge(天)、Compress等字段,缺一不可,否则默认不轮转 - 不要把
lumberjack.Logger直接传给gin.RecoveryWithWriter()—— 它实现了io.Writer,但没实现io.Closer;而gin.RecoveryWithWriter内部不会调用Close(),所以安全 - 若需同时记录访问日志和 panic 日志,别共用同一个
lumberjack.Logger实例——panic 日志要独立路径、独立轮转策略,避免被高频访问日志冲刷掉关键堆栈
示例片段:
errLog := &lumberjack.Logger{
Filename: "logs/panic.log",
MaxSize: 10, // MB
MaxBackups: 5,
MaxAge: 28, // 天
Compress: true,
}
r.Use(gin.RecoveryWithWriter(io.MultiWriter(errLog, os.Stderr)))
自定义 recovery 中间件时如何安全写轮转日志
如果你已手写 CustomRecovery()(比如要做 error 类型判断或报警),就别用 gin.RecoveryWithWriter,而应在 recover() 后主动调用日志写入。此时轮转逻辑仍靠 lumberjack.Logger,但注意三点:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
立即学习“go语言免费学习笔记(深入)”;
- panic 堆栈必须用
debug.Stack()获取,不能只打印err——否则丢失调用链 - 写日志前检查
c.Writer.Written(),防止 panic 发生在响应已写出后,导致重复写 header panic - 别在
defer里调用log.Fatal或os.Exit,这会让整个进程退出,轮转也就没意义了
轮转 writer 的生命周期应长于单个请求——它该是全局变量或初始化时一次创建,而不是每次 panic 都 OpenFile 一次。
为什么不用 logrus/zap 管理 panic 日志
可以,但没必要为 panic 日志单独引入重型日志库。原因很实际:
-
logrus默认不支持轮转,需额外加rotatelogs或lumberjack适配器,配置成本高 -
zap的RotateConfig仅限FileSink,且要求 Go 1.21+,老项目兼容性差 - panic 日志量极少(理想情况下每月不到十次),重点是「可靠落盘」和「易排查」,不是结构化或高性能
- 混用多个日志实例容易造成时间戳不一致、权限冲突(如两个进程同时写同一文件)
真正需要 zap/logrus 的是访问日志或业务审计日志——它们高频、需字段提取、常对接 ELK。panic 日志守住「不丢、可轮转、易定位」就够了。
轮转本身不难,难的是 panic 日志必须和访问日志物理隔离、写入时机必须严格卡在 recover 后且未写响应前——这两点漏掉任何一个,日志就可能丢失或污染。

















