go日志按时间滚动的核心机制是每次写入前判断并切换文件,而非定时检查;需用time.now().format("2006-01-02")生成时间戳文件名,加锁保护文件句柄切换,并避免lumberjack大小限制干扰。

Go 日志文件按时间滚动的核心机制是什么
Go 标准库 log 本身不支持自动滚动,必须借助第三方库或手动实现轮转逻辑。关键点在于:**不是“定时检查”,而是“每次写入前判断是否需要新建文件”**——这样避免后台 goroutine 管理时间、减少竞态和资源泄漏风险。
主流做法是用 os.OpenFile 配合文件名中的时间戳(如 app-2024-06-15.log),每次写日志前计算当前日期/小时,与当前打开的文件名比对;不匹配就关闭旧文件、打开新文件。
- 推荐用
time.Now().Format("2006-01-02")生成日切片名,注意 Go 时间格式化固定用"2006-01-02",不是"Y-m-d" - 不要用
time.Ticker定期切换文件——如果某段时间无日志,文件句柄仍被占用,且可能在 tick 时刻正写入中导致 panic - 多 goroutine 写日志时,必须加锁(
sync.Mutex)保护文件句柄切换过程
用 lumberjack 实现小时级滚动要改哪些配置
lumberjack.Logger 是最常用的日志轮转封装,但它默认按大小滚动。要改成纯时间驱动(比如每小时一个文件),需禁用大小限制,并手动控制文件名生成逻辑。
它本身不原生支持“按小时命名”,所以得绕过 Filename 字段的静态设定,改用自定义 io.Writer 封装:
type HourlyWriter struct {
mu sync.Mutex
current *os.File
pattern string // e.g. "logs/app-%s.log"
}
func (w *HourlyWriter) Write(p []byte) (n int, err error) {
now := time.Now()
hourKey := now.Format("2006-01-02-15") // 2024-06-15-14 表示14点整开始的小时
w.mu.Lock()
defer w.mu.Unlock()
if w.current == nil || !strings.Contains(w.current.Name(), hourKey) {
if w.current != nil {
w.current.Close()
}
filename := fmt.Sprintf(w.pattern, hourKey)
os.MkdirAll(filepath.Dir(filename), 0755)
w.current, err = os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return 0, err
}
}
return w.current.Write(p)
}
-
lumberjack的MaxSize/MaxAge全部设为 0,否则它会按自己逻辑覆盖或删除文件 - 不要直接把
lumberjack.Logger当io.Writer传给log.SetOutput,它内部有缓冲和锁,和时间判断逻辑冲突 - 注意
os.OpenFile的 flag:必须含os.O_APPEND,否则并发写可能覆盖内容
为什么 log.SetOutput 替换后 panic: “write /path/to/file: bad file descriptor”
这个错误几乎总是因为:**文件被关闭了,但仍有 goroutine 持有旧的 *os.File 引用并尝试写入**。
- 常见于未加锁的 Writer 实现——两个 goroutine 同时进入
Write,A 关闭文件,B 还在用老句柄写 - 也可能是 defer
Close()被提前触发(比如函数返回早于日志写完) - 还有一种隐性情况:用
log.New创建多个 logger,但共用同一个io.Writer实例,而该 writer 内部状态(如当前文件)被其中一个 logger 切换,另一个 unaware 导致写失败
验证方法:在 Write 开头加 if w.current == nil { return 0, errors.New("file closed") },看是否真被提前关掉。
生产环境必须考虑的三个边界问题
时间滚动看着简单,但线上容易出问题的点很具体:
- 服务器时区变更(如手动改系统时间或 NTP 校准)会导致文件名突变,旧文件被丢弃、新文件名重复——解决方案:用
time.Now().In(loc)固定使用time.UTC或指定时区,别依赖本地时区 - 高并发下频繁创建/关闭文件句柄,可能触发系统
ulimit -n限制——应复用已打开的文件句柄,只在必要时切换,而不是每次写都OpenFile - 日志路径磁盘满时,
OpenFile失败,但错误被忽略,后续所有日志静默丢失——必须检查err并 fallback 到 stderr 或 panic(至少告警)
最易被忽略的是:滚动逻辑里没处理 os.MkdirAll 失败,结果日志目录不存在,新文件创建失败,整个写入链路静默中断。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











