Go标准库log不支持日志滚动,必须用第三方库(如lvg/logrotator、lumberjack)或手动封装;前者轻量无依赖、按大小轮转、性能高但需预建目录并显式Close,后者功能全但配置复杂易踩坑。

为什么不用 logrotate 或第三方库直接集成?
因为部署环境受限(比如嵌入式设备或容器只跑单个 Go 进程),没法依赖系统级 logrotate;而 full-feature 的第三方日志库(如 logrus + rotatelogs)常带多余依赖或默认行为不符合轻量需求——比如强制用 io.MultiWriter、隐式创建子目录、或轮转时阻塞写入。真正轻量的核心是:自己控制文件句柄、按大小/时间触发、不 panic、不依赖外部二进制。
用 os.File + 定时检查实现最小轮转逻辑
Go 标准库没内置轮转,但完全够用:os.Stat 查大小、time.Now() 判时间、os.Rename 原子重命名旧文件、os.OpenFile 以 os.O_CREATE | os.O_APPEND | os.O_WRONLY 模式打开新文件。关键不是“怎么写”,而是“什么时候切”和“切的时候别丢日志”。
- 轮转触发条件必须可配置:支持按
maxSize(字节)、maxAge(小时)、maxBackups三者任一满足即触发 - 每次写日志前不做实时 stat —— 开销大;改用后台 goroutine 每 30 秒检查一次当前文件状态
-
os.Rename在同分区下是原子的,但跨分区会失败,所以务必确保日志目录在同一挂载点;失败时 fallback 到io.Copy+os.Remove,并记录 error 到 stderr - 轮转瞬间要避免写入丢失:先
file.Close(),再 rename,最后用新路径os.OpenFile,中间任何一步失败都保留原*os.File继续写
io.Writer 接口如何无缝对接标准 log.Logger?
Go 的 log.Logger 只认 io.Writer,所以不要自己造日志格式,而是包装一个线程安全的 io.WriteCloser 实现,内部持有一个指向当前 *os.File 的指针,并用 sync.RWMutex 保护切换过程。这样 log.SetOutput(yourWriter) 就能直接用,且多 goroutine 写日志不会 panic。
注意:Write([]byte) 方法里不能直接调 file.Write —— 如果此时后台正在轮转,file 可能已被关闭。正确做法是:RLock 读取当前 file,判空后 write;若写失败(比如 EBADF),再 RUnlock 后 Lock,触发一次强制轮转并更新 file 指针。
立即学习“go语言免费学习笔记(深入)”;
func (w *RotatingWriter) Write(p []byte) (n int, err error) {
w.mu.RLock()
f := w.file
w.mu.RUnlock()
if f == nil {
return 0, errors.New("writer closed")
}
return f.Write(p)
}时间轮转精度与 Windows 兼容性坑
按天轮转看似简单,但 time.Now().Format("2006-01-02") 生成的文件名在跨午夜时可能漏切——因为检查 goroutine 不一定刚好在 00:00:00 执行。实际做法是:记录上一次轮转的 dateStr,每次检查时算 today := time.Now().Format("2006-01-02"),仅当 today != lastDate 且 lastRotateTime.Add(24*time.Hour).Before(time.Now()) 才触发,避免高频误触。
Windows 下 os.Rename 对正在写的文件会返回 Access is denied 错误(不像 Linux 允许 rename open file)。必须先 file.Close() 再 rename,且 rename 后不能立即 OpenFile——得加 time.Sleep(10 * time.Millisecond) 让系统释放句柄,否则新文件打开失败。
轻量不是删功能,是把每个判断、每次 syscall、每处锁粒度都掂量清楚。最易被忽略的是:轮转失败后是否降级静默(继续写原文件)还是 panic(导致进程挂)——生产环境必须选前者。


















