
本文详解如何使用 fsnotify 监听新日志文件创建,并通过优雅终止旧 tail 实例、启动新实例,实现类似 tail -f 的持续滚动日志分析,避免 goroutine 泄漏和事件丢失。
本文详解如何使用 fsnotify 监听新日志文件创建,并通过优雅终止旧 tail 实例、启动新实例,实现类似 tail -f 的持续滚动日志分析,避免 goroutine 泄漏和事件丢失。
在构建日志分析器时,一个常见需求是:当系统按时间或大小轮转生成新日志文件(如 mylog_0001.log.txt)时,程序需自动停止对旧文件的 tail,立即切换至新文件并保持实时流式读取。你当前代码存在两个核心问题:主函数过早退出导致 goroutine 被强制终止,以及 tail.Tail 实例无法被安全复用或中断——因为 tailLog() 函数本身会阻塞在 for line := range t.Lines 循环中,导致后续逻辑(如 t.Stop())永不执行。
✅ 正确做法:分离监听与消费,显式控制生命周期
关键原则是:每个 tail.Tail 必须运行在独立 goroutine 中,并通过其 Stop() 方法主动终止;所有 goroutine 的启停需由主流程统一协调,且 main() 必须阻塞等待(例如用 sync.WaitGroup 或 select{})。
以下是重构后的完整可运行示例:
package main
import (
"fmt"
"log"
"strings"
"sync"
"time"
"github.com/fsnotify/fsnotify"
"gopkg.in/tail.v1"
)
// tailWorker 封装单个 tail 实例的启动、监听与安全退出
func tailWorker(filename string, done chan struct{}, wg *sync.WaitGroup) {
defer wg.Done()
t, err := tail.TailFile("/mydir/"+filename, tail.Config{
Follow: true,
ReOpen: true,
Location: &tail.SeekInfo{Offset: 0, Whence: 2}, // 从末尾开始
})
if err != nil {
log.Printf("failed to tail %s: %v", filename, err)
return
}
defer t.Close() // 确保资源释放
log.Printf("started tailing: %s", filename)
// 启动监听循环
for {
select {
case line, ok := <-t.Lines:
if !ok {
log.Printf("tail channel closed for %s", filename)
return
}
if strings.Contains(strings.ToLower(line.Text), "mfc") {
fmt.Printf("[MATCH] %s: %s\n", filename, line.Text)
// 这里添加你的业务逻辑(如告警、统计等)
}
case <-done:
log.Printf("stopping tail for %s", filename)
return // 优雅退出
}
}
}
func main() {
const dirPath = "myDir"
// 1. 初始化 fsnotify 监听器
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal("failed to create watcher:", err)
}
defer watcher.Close()
err = watcher.Add(dirPath)
if err != nil {
log.Fatal("failed to watch directory:", err)
}
// 2. 控制通道与同步原语
newFileCh := make(chan string, 10) // 缓冲通道防阻塞
stopCurrentTail := make(chan struct{})
var wg sync.WaitGroup
// 3. 启动初始 tail(假设首个文件已存在)
initialFile := "mylog_0000.log.txt"
wg.Add(1)
go tailWorker(initialFile, stopCurrentTail, &wg)
// 4. 文件创建监听 goroutine
go func() {
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
if event.Op&fsnotify.Create == fsnotify.Create {
log.Printf("detected new file: %s", event.Name)
newFileCh <- event.Name
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
log.Printf("watcher error: %v", err)
}
}
}()
// 5. 主调度循环:接收新文件 → 停止旧 tail → 启动新 tail
for filename := range newFileCh {
log.Printf("switching to new log file: %s", filename)
// 发送停止信号并等待旧 goroutine 退出
close(stopCurrentTail)
wg.Wait()
// 重置 stop 通道并启动新 worker
stopCurrentTail = make(chan struct{})
wg.Add(1)
go tailWorker(filename, stopCurrentTail, &wg)
}
}⚠️ 关键注意事项
-
tail.Tail不支持重复调用Stop():必须确保每次只对一个活跃实例调用Stop(),且Stop()后不可再读取t.Lines。 -
永远不要让
main()直接返回:本例中for filename := range newFileCh使main持续运行;若需退出机制,可额外监听os.Signal并关闭newFileCh。 -
路径拼接需严谨:
fsnotify返回的是相对路径,而tail.TailFile需要绝对路径,请根据实际目录结构调整(如使用filepath.Join())。 -
错误处理不可省略:生产环境应增加文件权限检查、路径合法性校验(如排除
.swp临时文件)及重试退避逻辑。 -
替代方案建议:对于高可靠场景,可考虑
github.com/nxadm/tail(v1 的现代维护分支),它原生支持Context取消,API 更清晰。
通过上述结构,你将获得一个健壮、可维护的日志轮转监听器——每个 tail 生命周期清晰可控,事件不丢失,资源不泄漏。

















