zap 本身不支持日志自动切割,必须配合 lumberjack 才能可靠实现按大小/时间切分;手动用 os.openfile + 时间判断易导致丢日志、并发写错、句柄不释放等问题。

zap 本身不支持日志自动切割,必须配合 lumberjack 或手动轮转逻辑才能实现按大小/时间切分。直接用 os.OpenFile + 时间判断(如标题里“按天切分”)容易丢日志、并发写错文件、不释放旧句柄。
为什么不能只靠 zap.NewCore + os.OpenFile 实现可靠切割
你看到的“每天新建一个文件”代码(比如用 FormattedNow("20060102") 拼路径再 os.OpenFile)在真实服务中会出问题:
- 多个 goroutine 同时写日志时,可能同时打开同一个文件,导致写入错乱或覆盖
- 凌晨切换日期后,旧文件句柄没关闭,
LogFileAccess还指向昨天的文件,新日志继续往旧文件写 - 没有锁保护,
LogFileAccess被并发修改,出现 nil pointer panic 或写入空文件 -
defer LoggerAccess.Sync()放在初始化函数里是无效的——它只在 SetupAccessLogger 返回前执行一次,不是每次写日志都刷盘
用 lumberjack.Writer 替代自定义文件轮转
lumberjack 是目前 Go 生态最稳定、被大量线上项目验证的日志切割方案。它内部做了文件句柄管理、原子重命名、并发安全写入,你只需把它当做一个 io.WriteSyncer 传给 zapcore.NewCore:
- 安装:
go get -u gopkg.in/natefinch/lumberjack.v2 - 配置示例(按大小 + 保留 7 天):
logWriter := &lumberjack.Logger{ Filename: "./logs/access.log", MaxSize: 100, // MB MaxBackups: 7, MaxAge: 7, // days Compress: true, } - 注意:
Filename是基础文件名,lumberjack自动加.2026-08-11.001后缀,不需要拼日期字符串 - 不要对
lumberjack.Logger做os.OpenFile包裹,它自己实现了Write和Sync
Gin 中替换默认 Logger 中间件
gin 的 Logger() 默认输出到 gin.DefaultWriter,你要让它走 zap,就得写一个中间件替代它:
- 别用
gin.DefaultWriter = io.MultiWriter(...)—— 这种方式无法注入结构化字段(如status、cost),也绕不开lumberjack的写入控制 - 用
GinLogger(logger *zap.Logger)函数(见知识库中已给出的实现),把zap.Logger作为参数传进去 - 确保这个中间件在
r.Use(...)中注册顺序早于业务 handler,否则c.Next()后拿不到响应状态码和耗时 - 如果同时要记录 error 日志,单独配一个
errorWriter(同样用lumberjack),再用zapcore.NewTee合并 core
zapper.Logger 与 lumberjack 的组合陷阱
常见错误配置:
-
lumberjack.MaxAge设为 0:表示永不清除备份,磁盘迟早打满 - 把
lumberjack.Logger当成普通*os.File调Close():会破坏内部状态,后续写入 panic - 在
zapcore.NewCore中传入zapcore.AddSync(lumberjackWriter)后,又额外套一层zapcore.BufferedWriteSyncer:造成双缓冲,日志延迟不可控且可能丢失 - 没调
logger.Sync()就退出进程:最后几 KB 日志永远写不到磁盘(尤其在 systemd 或 k8s 里 SIGTERM 后立即 kill)
真正关键的点就两个:用 lumberjack 接管文件 IO,用 defer logger.Sync() 放在 main() 函数退出前——其他所有“自动”都是假象。











