Go中实现日志轮转需自主控制:按时间切须基于滚动边界(如当日零点)判断过期,而非time.Now();按大小切应使用file.Seek(0,2)获取真实长度;并发时须用Mutex保护整个轮转流程,并注意时区与符号链接处理。

logrotate 用不了?直接在 Go 里做轮转更可控
Go 标准库的 log 包不带轮转,os.File 本身也不会自动切文件。想按天、按大小切日志,得自己搭逻辑——不是加个第三方库就完事,关键得清楚触发条件怎么判、旧文件怎么命名、并发写会不会丢日志。
按时间切日志:别只看 time.Now(),得盯住“滚动边界”
常见错误是每写一条就检查一次时间,比如每次调用 log.Println() 都算今天是不是变了——性能差,还容易在跨天瞬间漏切。正确做法是:只在写入前检查当前文件是否“已过期”,且这个“过期”要基于滚动周期(如每天零点)而非系统时间戳比对。
- 用
time.Date(year, month, day, 0, 0, 0, 0, loc)算当天零点作为滚动基准,不是time.Now().Truncate(24*time.Hour)(后者在时区或夏令时下可能偏移) - 记录当前日志文件的“有效起始时间”,比如打开
app-2024-06-15.log时存下2024-06-15T00:00:00+08:00,后续写入前比对当前时间是否 >= 下一周期起点 - 避免用
os.Stat().ModTime()判断是否该切——文件可能长时间没写,但日期已变,靠修改时间会误判
按大小切日志:os.File.Seek(0, 2) 比 os.Stat() 更准
查文件大小时,os.Stat() 返回的是内核缓存值,如果文件被其他进程截断或重定向,它可能滞后;而 os.File.Seek(0, 2) 直接跳到末尾并返回真实偏移量,是当前文件句柄视角下的准确长度。
- 每次写入前先
file.Seek(0, 2),拿到当前位置即当前大小,再跟阈值(如100 * 1024 * 1024)比较 - 切之前必须
file.Close(),否则 Windows 下会报The process cannot access the file because it is being used by another process - 重命名旧文件时,用
os.Rename()而非os.Copy()+os.Remove(),前者原子、快,后者在大文件场景易中断导致日志丢失
并发安全:别让多个 goroutine 同时触发轮转
如果你用 log.SetOutput() 换句柄,又没加锁,两个 goroutine 几乎同时发现该切了,就可能一个 rename 成功,另一个 rename 失败(文件已不存在),或者两个都 rename 成同一目标名,后者覆盖前者。
立即学习“go语言免费学习笔记(深入)”;
- 用
sync.Mutex包住“判断是否需轮转 + 关闭旧文件 + 打开新文件 + 更新输出句柄”整个流程 - 不要在
Write()方法里做耗时操作(比如压缩、上传),轮转逻辑必须轻量,否则阻塞所有日志写入 - 考虑用
chan struct{}把轮转请求发到单独 goroutine 处理,主写入路径只发信号,但要注意 channel 满了会阻塞——得设缓冲或用select带 default
最麻烦的其实是时区和符号链接:如果日志路径是软链,os.Stat() 和 os.Lstat() 行为不同;如果服务部署在 UTC 容器但业务按本地时间切,time.Now().In(loc) 的 loc 必须显式传入,不能依赖 time.Local——后者在容器里常为空。










