slog 和 log 默认不支持日志分割,必须借助 lumberjack 等第三方库实现按大小或时间轮转;lumberjack 需包装 io.writer,配置 maxsize、maxbackups、maxage 等参数控制切割行为,注意权限、压缩阻塞及并发安全问题。

log/slog 默认不支持日志分割,别白费劲配它
Go 标准库 slog 和老版 log 都是纯内存写入,没有轮转(rotation)、大小限制、按日期切分等能力。你配了 SetOutput 指向一个文件,它就一直往里追加,直到磁盘爆掉或运维半夜被报警叫醒。
常见错误现象:log file grows endlessly、disk usage 98%、日志查不到“昨天”的内容(其实全堆在同一个超大文件里)。
- 想用标准库做切割?做不到。它连
os.File的Stat()都不调,更不会判断大小或时间 - 第三方库才是正解:优先考虑
lumberjack(稳定、轻量、无依赖),次选zerolog+rotator或zap+lumberjack组合 -
lumberjack是包装器,不是替代品——它必须套在io.Writer上,比如slog.NewTextHandler(file, opts)的file就得是&lumberjack.Logger{...}
用 lumberjack 实现按大小+备份数切割(最常用场景)
这是线上服务最稳妥的配置:避免单文件过大,控制总磁盘占用,且不依赖系统 cron 或外部脚本。
关键参数含义直给:Filename 是当前写入文件路径;MaxSize 单位是 MB(注意不是字节);MaxBackups 是保留几个旧文件;MaxAge 是保留几天(和备份数是“或”关系,满足任一即清理)。
logger := &lumberjack.Logger{
Filename: "/var/log/myapp/app.log",
MaxSize: 100, // megabytes
MaxBackups: 7,
MaxAge: 28, // days
Compress: true,
}
- 别设
MaxSize: 1—— 频繁切分会导致 inode 暴增、小文件过多,IO 压力反升 -
Compress: true会用gzip压缩归档文件,但压缩过程阻塞写入,高吞吐场景慎开 - 确保进程对
Filename所在目录有写权限,否则切分时静默失败(只留 error log 在 stderr,容易漏看) - 如果用 systemd,记得在 service 文件里配
ReadWritePaths=/var/log/myapp,否则切分失败且无提示
按时间切分(如每天一个文件)需要自己补逻辑
lumberjack 本身不支持 app-2024-06-01.log 这种命名,它只认 app.log.1.gz 这种序号式归档。真要日期格式,得自己实现 io.WriteCloser,或换库。
简单可行方案:用 robfig/cron 每天 00:00 关闭旧文件、打开新文件,配合 slog.New 动态换 handler。但要注意并发安全——slog.Handler 不保证线程安全,不能边写边换。
- 推荐折中法:仍用
lumberjack,但把Filename设为带日期的路径,例如/var/log/myapp/app-$(date +%Y-%m-%d).log,再用 systemd timer 或 crontab 每日触发程序重启(适合低频更新服务) - 若必须热切换,得加锁 + 双 buffer:新 handler 初始化好后,原子替换指针,并等正在写的 record 完成才关旧文件
- 别信网上“用
time.Ticker每秒检查时间”的做法——精度差、goroutine 泄漏、且切分时刻不一致,多实例时文件名还可能冲突
zap + lumberjack 组合的坑比想象中多
很多人图 zap 性能好,又想切分,就直接把 lumberjack.Logger 塞给 zapcore.AddSync。表面跑得通,但容易在高压下丢日志。
根本原因:lumberjack 的 Write 方法在触发轮转时会锁全局 mutex,并可能调 os.Rename 和 os.Remove —— 这些是系统调用,耗时不可控。而 zap 的 syncer 默认是阻塞写,一旦卡住,整个日志 pipeline 就堵死。
- 必须配
zap.AddCallerSkip(1)之类时,别跳过lumberjack的栈帧,否则行号指向错文件 - 生产环境务必开启
lumberjack的LocalTime: true,否则归档文件名里的时间是 UTC,和日志内容时间戳对不上 - 测试切分是否生效,别只看文件生成,要用
du -sh /var/log/myapp/*.log*看实际大小,并手动touch修改文件时间触发MaxAge清理逻辑
真正难的不是写几行代码让日志切开,而是切的时候不卡主业务、不丢关键错误、清理时不误删、排查时不懵圈。这些细节藏在 lumberjack 的 mutex 里、os.Rename 的 errno 里、还有你没加锁的 handler 替换里。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











