zap默认不支持按天切割,因其设计原则是“不做文件管理”,只负责写日志;真正按自然日切割需自行封装线程安全的RotateWriter,在每次Write前比对日期、加锁关闭旧文件并打开带日期的新文件。

为什么 zap 默认不支持按天切割?
zap 本身是高性能结构化日志库,核心设计原则是「不做文件管理」——它只负责写日志,不负责轮转、压缩、删除。所以直接调用 zap.NewProduction() 或 zap.NewDevelopment(),日志会一直追加到单个文件里,不会自动按天切分。
要实现按天切割,必须借助第三方日志写入器(WriteSyncer),最常用的是 lumberjack,但它按大小或时间(秒级)轮转,**不原生支持“按自然日”(00:00–23:59)切割**。真正支持按天切割的方案,得自己控制文件名 + 定时切换写入器。
用 zapcore.NewMultiWriteSyncer + 自定义 RotateWriter 实现每日新建文件
关键思路:不依赖轮转库的定时机制,而是每次写日志前检查当前日期是否变化,若变化则关闭旧文件、打开新文件,并把新 io.WriteSyncer 注入到 zapcore.Core 中。这需要封装一个线程安全的可切换写入器。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用
sync.RWMutex保护当前写入器和文件名状态 - 每次
Write(p []byte)前调用checkAndRotate(),比较time.Now().Format("2006-01-02")和当前文件日期 - 新文件路径建议包含日期,例如
log/app-2024-05-20.log - 务必在
RotateWriter.Close()中关闭旧文件句柄,否则 Linux 下可能触发too many open files - 不要在
Write()中做阻塞操作(如磁盘 IO),把文件打开延迟到checkAndRotate()的非热路径上
简短示意(省略 error 处理和锁细节):
func (w *RotateWriter) Write(p []byte) (n int, err error) {
w.mu.RLock()
defer w.mu.RUnlock()
w.checkAndRotate()
return w.current.Write(p)
}
func (w *RotateWriter) checkAndRotate() {
today := time.Now().Format("2006-01-02")
if today == w.date {
return
}
w.mu.Lock()
defer w.mu.Unlock()
if today == w.date { // double-check
return
}
w.current.Close() // 关闭旧文件
filename := fmt.Sprintf("log/app-%s.log", today)
f, _ := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
w.current = zapcore.AddSync(f)
w.date = today
}
lumberjack 能不能凑合用?要注意什么?
可以凑合,但不是真正“按天”。lumberjack.Logger 支持 MaxAge(单位:天),但它含义是「文件最多保留 N 天」,不是「每天零点切新文件」;它的 LocalTime 仅影响清理逻辑,**不触发切割时机**。实际切割由 MaxSize(MB)或 MaxBackups 驱动。
常见误用现象:MaxAge: 1 被当成“每天切”,结果日志还是累积到一个大文件里,直到满 100MB 才切,且旧文件可能凌晨 3 点才被删掉。
如果坚持用 lumberjack,唯一接近按天的方式是设极小 MaxSize(如 1KB),靠频繁写触发切割——但这会产生大量小文件,且无法保证零点对齐,不推荐。
生产环境必须处理的三个细节
按天切日志看着简单,线上容易翻车的点很具体:
-
os.OpenFile要用os.O_CREATE | os.O_WRONLY | os.O_APPEND,漏掉O_APPEND会导致每天覆盖而非追加 - 程序启动时,如果当天日志文件已存在,要确保能正常打开并追加,而不是报错退出——
os.OpenFile在文件存在时不会失败 - 多进程场景(比如用
systemdreload 或 k8s rolling update)下,多个实例可能同时往同名文件写,需加文件锁(syscall.Flock)或改用进程 ID 后缀隔离
真正的按天切割不是配个参数就完事,得自己管住文件生命周期、时区一致性(用 time.Local 还是 time.UTC)、以及进程重启时的续写逻辑。这些地方一松懈,第二天就找不到日志了。


















