最稳妥方案是用 lumberjack 按大小轮转,MaxSize 单位为 MB(非字节),设 10 即 10MB;MaxBackups 需设具体值(如 7),禁用 0;必须开启 LocalTime=true,进程退出前显式调用 Close(),且路径父目录需预先创建。

Go 标准库的 log 和 slog 都不支持日志切割,硬配无效,必须换方案。
用 lumberjack 按大小切最稳,但参数单位和关闭逻辑容易错
它不是“加个库就能跑”,关键在配置是否踩中默认陷阱:
-
MaxSize单位是 MB(不是字节),设10就是 10MB;写成10 * 1024 * 1024反而变成 10GB,切不动 -
MaxBackups设0表示不限制——线上磁盘会涨满,必须设具体值,比如7 -
LocalTime: true必须开,否则归档文件名里的时间戳是 UTC,和你本地ls -l看到的时间对不上 - 进程退出前没调
lj.Close(),最后几条日志大概率卡在缓冲区没落盘;defer不可靠,得在signal.Notify的 handler 里显式关
按天切日志?lumberjack 的 MaxAge 不等于“每天零点新建”
MaxAge 是按文件“最后修改时间”判断是否归档,不是按写入时间或日历日期。这意味着:
- NTP 校正导致系统时间跳变时,可能误触发归档,或跳过该归档
- 它无法保证每天 00:00 创建新文件——比如凌晨 00:01 才有第一条日志,那当天所有日志都进同一个文件
- 真要严格按天切,得自己起
time.AfterFunc:算出当天结束时间戳,到点Rotate(),再 schedule 下次;注意用time.Now().In(loc)显式指定时区,别依赖空的本地时区(容器里常为空)
自己写轮转 Writer 要守住三个原子动作
不是“检查大小→重命名→新建文件”分开写就行,中间任何一步被并发打断都会丢日志或 panic:
立即学习“go语言免费学习笔记(深入)”;
- 所有操作必须包裹在同一个
sync.Mutex里:获取当前文件句柄 →f.Stat().Size()检查实时大小(别缓存字节数)→ 关闭旧文件 →os.Rename重命名 →os.Create新建(权限显式设0644)→ 写入 - Windows 下
os.Rename对正在打开的文件会失败,必须先f.Close()再 rename - 压缩别在 Go 里做:
exec.Command("gzip", "-f", oldFilename)异步扔出去就行;原生compress/gzip处理 >100MB 文件容易 OOM 或卡主线程
别忽略日志输出目标本身是否可靠
哪怕轮转逻辑写得再严丝合缝,如果日志根本没写进文件,一切白搭:
- 用
os.OpenFile(..., os.O_APPEND)后手动删了文件,程序还在往已删除的 fd 写——不报错,但内容丢了 - 多实例共用一个
*lumberjack.Logger,或每个 goroutine 自己 new 一个,会导致竞争、重复 close、write: bad file descriptor - 路径父目录不存在就 panic:用
lumberjack前必须os.MkdirAll(filepath.Dir("app.log"), 0755);用logrotator也一样,它不自动建目录
真正难的不是“怎么切”,而是“什么时候切不丢、切完谁来读、切多了谁来删”。轮转只是链条一环,文件权限、信号处理、归档路径隔离、退出清理,漏一个就在线上埋雷。


















