log.setoutput直接换文件会导致日志丢失,因其非原子操作且无同步保护,旧文件句柄可能正被写入而被关闭,新文件尚未就绪,多goroutine下易出现日志写入混乱或静默丢弃。

为什么 log.SetOutput 直接换文件会导致日志丢失
Go 标准库的 log.Logger 默认不缓冲,写入是同步且阻塞的;但一旦你手动用 os.OpenFile 创建新文件并调用 log.SetOutput 切换输出目标,旧句柄可能仍在写入中,而新文件句柄还没完成 flush —— 这时如果恰好有并发日志写入,就可能丢几条。
更关键的是:log.SetOutput 不做任何同步保护,也不是原子操作。多 goroutine 场景下,两个 goroutine 可能同时执行切换+写入,导致一个写到旧文件、一个写到新文件,但你根本不知道哪条去了哪边。
- 别在日志写入过程中动态调用
log.SetOutput - 切换必须发生在无写入窗口期,或由单个协调 goroutine 控制
- 推荐用带锁的 writer 封装,而不是裸切
os.File
用 lumberjack.Logger 实现按天切割最简可行方案
lumberjack 是事实标准,它内部用 sync.Mutex 保护写入,并在每次写前检查是否需 rotate(比如时间戳跨天),触发时自动 rename + reopen,整个过程对上层 io.Writer 接口透明。
注意它默认按大小切割,要按天得关掉 MaxSize,只开 MaxAge 和 LocalTime:
lw := &lumberjack.Logger{
Filename: "/var/log/app.log",
MaxAge: 1 * 24 * time.Hour, // 必须设,否则不按天切
LocalTime: true, // 否则用 UTC,和系统 cron 不对齐
Compress: false, // 压缩会额外开进程,按天场景通常不需要
}
然后直接传给 log.SetOutput(lw) 即可。它会在每天 0 点(本地时区)自动 rename 成 app.log.2024-06-15 并新建空文件。
-
MaxAge=24*time.Hour是必要条件,不是“最多保留 24 小时”,而是“文件存活满 24 小时后才允许被 rotate” - 首次启动时若已有同名文件,
lumberjack会读其 mtime 判断是否该切,所以确保系统时间准确 - 不要设
MaxBackups过小(如 1),否则旧日志会被立即删掉,跨天瞬间可能丢数据
自己实现按天 rotate 的核心逻辑要点
如果因合规或审计要求必须自研(比如禁止第三方依赖),关键不在“怎么切”,而在“什么时候切”和“怎么切得安全”:
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
- 用
time.Ticker每分钟检查一次当前日期是否变化,而不是靠写日志时判断 —— 避免低频服务当天无日志就不切 - rotate 操作必须加全局 mutex,且要在
Close()旧文件后、OpenFile新文件前完成 rename(否则 Windows 下会报 “The process cannot access the file”) - 新文件必须用
os.O_CREATE | os.O_APPEND | os.O_WRONLY打开,且Chmod设权限(默认 0600,运维常需要 0644) - 务必用
syscall.Flock(Linux)或syscall.LockFileEx(Windows)做文件级互斥,防止多个进程实例同时切同一文件
示例判断逻辑:
now := time.Now()
if now.Year() != lastRotate.Year() || now.Month() != lastRotate.Month() || now.Day() != lastRotate.Day() {
// 执行 rotate
lastRotate = now
}
日志路径含变量时 lumberjack 无法自动处理
lumberjack 的 Filename 是固定字符串,不支持模板如 /var/log/app-{env}.log 或按服务名动态生成路径。一旦你运行多个实例(比如 dev/staging/prod),它们会互相覆盖同一个文件。
解决办法只有两个:
- 启动时根据环境变量生成唯一
Filename,比如fmt.Sprintf("/var/log/app-%s.log", os.Getenv("ENV")),然后每个实例用独立lumberjack.Logger - 彻底放弃
lumberjack,改用zap+zapcore.NewMultiWriteSyncer+ 自定义RotateSyncer,它支持 runtime 决定文件名
后者复杂度陡增:你要自己维护每个文件的 last modified 时间、做文件锁、处理 panic 时的 close race —— 多数团队最后都退回第一种方案。
真正难的从来不是“怎么切”,而是“怎么让多个进程/实例不打架”,这点容易被忽略。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










