
logrotate 逻辑怎么用 Go 做出来
Go 标准库没有内置日志轮转,os.Rename 和 os.Stat 是核心,但直接套用容易丢日志或并发写崩。轮转本质是「原子重命名 + 条件判断」,不是简单复制粘贴。
常见错误现象:rename /a.log /a.log.1: invalid cross-device link(跨文件系统失败);多个 goroutine 同时触发轮转导致 a.log.1 被覆盖;轮转后新日志仍写入旧文件句柄(没刷新 *os.File)。
- 先用
os.Stat检查文件大小或修改时间,别依赖内存计数(进程重启就丢) - 轮转前用
os.Rename,失败时 fallback 到io.Copy+os.Remove(仅当同设备失败才走) - 轮转后必须调用
file.Close()再os.OpenFile(..., os.O_CREATE|os.O_APPEND|os.O_WRONLY)获取新句柄
为什么不能只用 io.MultiWriter 做轮转
io.MultiWriter 只是分发写入,不解决文件切分逻辑。它没法感知当前文件是否该切、切完怎么关旧句柄、新文件权限怎么设——这些都得自己兜底。
使用场景:适合做「日志同时写本地+网络」的旁路分发,但和轮转无关。真要轮转,必须控制底层 *os.File 的生命周期。
立即学习“go语言免费学习笔记(深入)”;
-
io.MultiWriter里任意一个 writer panic 或阻塞,整个写入卡住(比如网络 writer 超时) - 轮转时如果只替换其中一个 writer,其他 writer 还在往旧文件写,数据就分裂了
- 文件权限、
Sync()行为、SetDeadline等都得单独处理,MultiWriter不透出这些接口
os.OpenFile 的 flag 组合陷阱
轮转后重新打开文件,os.O_CREATE | os.O_APPEND | os.O_WRONLY 是安全组合,但漏掉 os.O_SYNC 或权限参数,会导致日志丢失或权限错误。
参数差异:os.O_TRUNC 会清空文件,轮转后绝对不能带;os.O_RDWR 没必要,日志只是追加写;0644 在容器或 rootless 环境下可能被 umask 截断,建议显式传 0600 或 0644。
- Linux 下
os.O_APPEND保证 write 原子性,但 Go 的WriteString不一定 —— 必须用Write+Sync()配合 - Windows 对
os.Rename锁文件更严格,轮转前确保没 goroutine 正在写(用sync.RWMutex控制写入口) - 如果日志量大,
os.O_SYNC会拖慢性能,可折中用定期file.Sync()(比如每 100 行)
测试轮转逻辑时最容易忽略的点
本地跑一次 go run main.go 看到文件生成了,不代表轮转可靠。真正的问题藏在边界条件里:磁盘满、文件被外部进程占用、时钟回拨、SIGTERM 中断轮转。
可给出简短示例:用 touch -d "2020-01-01" app.log 手动改时间戳,再启动程序看是否触发按天轮转;用 lsof app.log 模拟文件被占用,验证 os.Rename 失败后的 fallback 是否生效。
- 测试时禁用真实写入,把
os.OpenFile替换为io.Discard或bytes.Buffer,专注测轮转路径 - 用
filepath.Join(os.TempDir(), "test.log")而不是当前目录,避免污染项目文件 - 轮转函数返回 error,但主循环不能 panic —— 要记录错误并继续监听下一次触发条件
轮转看着简单,实际要扛住磁盘 IO、并发写、信号中断、权限变更四层压力。每个 os 调用后面都得想清楚失败怎么兜底,而不是默认它一定成功。










