最稳妥方案是直接用 lumberjack.Logger 配合 zap 或 logrus,因 Gin 原生不支持自动切分,自实现 os.File 易丢日志、并发写错位、SIGTERM 时 flush 不全,且无法自动创建目录或轮转。

直接用 lumberjack.Logger 配合 zap 或 logrus 是最稳妥的方案,Gin 原生不支持按大小/时间自动切分,自己实现 io.Writer 容易丢日志、并发写错位、SIGTERM 时 flush 不全。
为什么不能直接替换 gin.DefaultWriter 为自定义 os.File
常见错误是这么写:
file, _ := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
gin.DefaultWriter = file
问题很实际:
- 文件不会自动切分,
app.log会无限增长 - 进程重启或 SIGTERM 时,
file没有Close()或Sync(),最后一段日志大概率丢失 - 多 goroutine 写同一个
*os.File虽然线程安全,但没做缓冲控制,高并发下 write 系统调用频繁,性能差 - 目录不存在时
OpenFile失败,而lumberjack会自动创建父目录
lumberjack.Logger 的关键参数含义和坑点
lumberjack 是目前 Go 生态最成熟的日志轮转 Writer,但它几个字段单位和行为容易误用:
立即学习“go语言免费学习笔记(深入)”;
-
MaxSize单位是 MB(不是字节),设成10表示 10 MB,不是 10240 字节 -
MaxBackups控制保留的「已归档」文件数,比如error-2026-08-11T10-22-33.log这类带时间戳的文件;error.log(当前写入文件)不计入此数 -
MaxAge是天数,指归档文件最多保留多少天,不是从首次写入开始算,而是从该归档文件创建时间起算 -
Compress: true会在每次 Rotate 后立即调用gzip压缩对应归档文件,阻塞写入流程;生产环境建议设false,另起定时任务异步压缩 - 所有路径(如
Filename)必须包含完整目录,例如"./log/error.log",lumberjack会自动创建./log目录
Zap + lumberjack 实现双输出(终端 + 文件)并按业务分离
一个典型 Gin 项目需要访问日志、错误日志、运行日志三类,每类都要独立切割。核心是为每类日志构造独立的 lumberjack.Logger 和 zapcore.Core:
// 访问日志(只写文件,不打屏)
accessWriter := zapcore.AddSync(&lumberjack.Logger{
Filename: "./log/access.log",
MaxSize: 50,
MaxBackups: 14,
MaxAge: 30,
Compress: false,
})
// 错误日志(文件 + 终端双写)
errorWriter := zapcore.NewMultiWriteSyncer(
zapcore.AddSync(&lumberjack.Logger{
Filename: "./log/error.log",
MaxSize: 10,
MaxBackups: 7,
MaxAge: 90,
Compress: false,
}),
zapcore.AddSync(os.Stdout),
)
// 构造不同 core,再用 zap.Combine 合并
core := zapcore.NewTee(
zapcore.NewCore(encoder, accessWriter, zapcore.InfoLevel),
zapcore.NewCore(encoder, errorWriter, zapcore.ErrorLevel),
)
logger := zap.New(core)
注意:
- 不要把多个日志类型共用同一个
lumberjack.Logger实例,否则 Rotate 会互相干扰 - 如果用
zapcore.NewMultiWriteSyncer把终端和文件写入合并到一个 core,那终端也会输出 error 级别以下的日志,不符合「错误日志才上屏」的预期 - 务必在
main()结尾调用logger.Sync(),否则进程退出前最后一批日志可能滞留在 buffer 中
Gin 的 gin.LoggerWithWriter 怎么接入 lumberjack
Gin 自带的 gin.Logger() 中间件默认只写 gin.DefaultWriter,要让它写进 lumberjack,得显式传参:
accessLogWriter := zapcore.AddSync(&lumberjack.Logger{
Filename: "./log/gin-access.log",
MaxSize: 100,
MaxBackups: 30,
MaxAge: 7,
Compress: false,
})
r := gin.New()
r.Use(gin.LoggerWithWriter(accessLogWriter)) // ← 关键:传进去
r.Use(gin.Recovery())
但要注意:
-
gin.LoggerWithWriter输出的是纯文本格式,不是 JSON;如果要用结构化日志,得自己写中间件,基于c.Request和c.Errors手动打logger.Info/logger.Error - 它不区分 HTTP 状态码,所有请求都记为 info 级别;错误日志还得靠
Recovery()或自定义 panic 捕获逻辑补全 - 若同时启用
gin.LoggerWithWriter和你自己的 zap logger,需确保两者写入路径不冲突,否则可能看到重复日志
真正难的不是配置参数,而是理解 lumberjack 的 Rotate 触发时机——它只在 Write 时检查当前文件是否超限,所以空闲服务不会产生归档文件;另外,压缩必须等 Rotate 完成后才开始,如果 Compress: true 且日志量大,Rotate 可能卡住几秒,这点在压测或上线前一定要实测。


















