lumberjack本身不支持按天切割,仅依据文件大小或数量轮转;maxage是清理策略而非切割触发器,只删除超期备份而不触发每日重命名;真正实现“每天一个文件”需手动拼接带日期的filename,并在每次write前比对日期、加锁关闭旧文件并打开新文件。

lumberjack 本身不支持按天切割,它只响应文件大小或数量阈值;所谓“按天轮转”,必须由你控制 Filename 拼接逻辑并在每次 Write 前比对日期、加锁切换文件。
为什么 lumberjack 的 MaxAge 不是“每天切一次”
MaxAge 是清理策略,不是切割触发器。它只在每次写入前检查旧文件的 ModTime(),决定是否删除(比如删掉 7 天前的 app.log.1),但不会导致当天的 app.log 在零点被重命名。
- 常见错误现象:
app.log从 5 月 21 日下午写到 5 月 22 日上午,文件名没变、内容跨天 - 设了
MaxAge: 1,结果第二天日志还在往老文件里写,只是旧备份被删了 -
MaxAge: 0表示禁用基于时间的清理,仅靠MaxBackups控制保留数量 -
LocalTime: true必须开启,否则ModTime()是 UTC,和系统时区对不上,清理可能误判 - 它不解析日志内容里的日期字段,只看文件元信息
如何真正实现“每天一个文件”且不丢日志
核心是:每次 Write(p []byte) 前判断当前日期是否变化,变了就关旧、开新,并确保整个过程加锁、原子、无竞态。
- 封装一个结构体,缓存当前
Filename和today字符串(如"2026-06-01") - 每次
Write先调time.Now().Format("2006-01-02")对比,不一致则调用Close()+ 重建*lumberjack.Logger - 必须用
sync.Mutex包住整个判断 + 关闭 + 打开流程,否则并发写时可能往已关闭的文件句柄写,触发write: bad file descriptor - 不要用
time.AfterFunc或 goroutine 定时轮询——进程重启后定时器丢失,且零点毫秒级漂移会导致漏切
按大小轮转更可靠,MaxSize 配置要小心
按大小轮转是 lumberjack 最稳定、最可控的模式,但参数理解错容易踩坑。
-
MaxSize单位是 MB,不是字节;设MaxSize: 10表示单个文件最多 10MB,超了就自动轮转 - 别依赖写入字节数累加判断是否超限——缓冲未 flush 会导致误判;要用
f.Stat().Size()获取实时大小 -
os.Rename是原子操作,大文件也快;os.Copy会阻塞写入线程,不能用于轮转 - Windows 下
os.Rename对正在打开的文件会失败,必须先f.Close()再 rename - 新文件权限要显式设为
0644,否则默认0600,运维查日志时可能没权限读
多 goroutine 写日志时,lumberjack.Logger 本身不保证并发安全
文档没承诺线程安全,实际内部虽有锁但不可依赖;高频写入下容易出现竞态。
- 不要在多个 goroutine 里共用同一个
lumberjack.Logger实例 - 别用
log.SetOutput动态切换——它不加锁,多协程同时调用会截断日志或写入旧文件 - 自己封装一个
Write(p []byte)方法,统一用sync.RWMutex控制:获取句柄 → 检查大小/日期 → 轮转(如有)→ 写入 - 轮转期间允许只读(
RWMutex.RLock),但写必须独占(RWMutex.Lock),否则rename和write可能交叉
最容易被忽略的是:轮转动作必须显式 Close() 旧文件,否则缓冲未刷出,日志就丢了;而 Close() 后再 Open 新文件的间隙,如果没有锁保护,必然出现 write: bad file descriptor。这个边界条件在压测或零点高峰时一定会暴露。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











