如何重复启停Goroutine以应对日志文件轮转问题?
解决Golang日志回滚时的文件读取goroutine启停问题
嘿,同为刚入门Golang的新手,太懂你遇到日志回滚这种问题时的头大了!我之前也踩过几乎一模一样的坑,给你分享个亲测有效的实现思路,核心是用信号通道来精准控制goroutine的启停,配合文件系统监听工具(你提到的ExampleNewWatcher应该就是fsnotify库的示例吧?这个库非常适合做文件变化监听)。
核心思路
日志回滚的本质是:原chats.log被重命名/移走,新的chats.log被创建。我们需要:
- 监听目标文件所在的目录(而不是单个文件),捕获文件的创建、删除、重命名事件
- 用一个专属的停止通道(
stopChan)控制每个读取goroutine的生命周期 - 检测到回滚事件时,先关闭旧的停止通道让旧goroutine退出,再启动新的读取goroutine处理新文件
完整代码示例
package main import ( "bufio" "fmt" "os" "strings" "time" "github.com/fsnotify/fsnotify" ) func main() { targetFile := "chats.log" // 存储当前活跃的读取goroutine的停止信号通道 var currentStopChan chan struct{} // 初始化文件系统监听器 watcher, err := fsnotify.NewWatcher() if err != nil { fmt.Printf("创建文件监听器失败: %v\n", err) return } defer watcher.Close() // 确定要监听的目录(如果是相对路径,默认当前目录) watchDir := "." if strings.Contains(targetFile, "/") { watchDir = targetFile[:strings.LastIndex(targetFile, "/")] } // 添加目录监听 if err := watcher.Add(watchDir); err != nil { fmt.Printf("添加监听目录失败: %v\n", err) return } // 启动初始的日志读取goroutine currentStopChan = startLogReader(targetFile) // 监听事件循环 for { select { case event, ok := <-watcher.Events: if !ok { return } // 触发日志切换的三种场景:原文件被删除/重命名,新文件被创建 isRollEvent := (event.Op&fsnotify.Remove == fsnotify.Remove && event.Name == targetFile) || (event.Op&fsnotify.Rename == fsnotify.Rename && event.Name == targetFile) || (event.Op&fsnotify.Create == fsnotify.Create && event.Name == targetFile) if isRollEvent { fmt.Println("检测到日志回滚,切换至新文件...") // 停止旧的读取goroutine close(currentStopChan) // 短暂等待确保旧goroutine完全退出(可选,视业务场景调整) time.Sleep(50 * time.Millisecond) // 启动新的读取goroutine currentStopChan = startLogReader(targetFile) } case err, ok := <-watcher.Errors: if !ok { return } fmt.Printf("监听器错误: %v\n", err) } } } // startLogReader 启动一个读取指定日志文件的goroutine,返回用于停止该goroutine的通道 func startLogReader(filename string) chan struct{} { stopChan := make(chan struct{}) go func() { // 处理文件可能还未创建的情况,循环尝试打开 var file *os.File var err error for { file, err = os.Open(filename) if err == nil { break } // 如果是文件不存在,短暂重试,否则退出 if os.IsNotExist(err) { fmt.Printf("文件%s暂未创建,1秒后重试...\n", filename) time.Sleep(1 * time.Second) select { case <-stopChan: fmt.Println("读取goroutine未启动就收到停止信号,退出") return default: } } else { fmt.Printf("打开文件%s失败: %v\n", filename, err) return } } defer file.Close() fmt.Printf("开始读取文件: %s\n", filename) scanner := bufio.NewScanner(file) for { select { case <-stopChan: fmt.Printf("停止读取文件: %s\n", filename) return default: if scanner.Scan() { // 这里替换成你实际的日志处理逻辑 line := scanner.Text() fmt.Printf("[日志] %s\n", line) } else { // 读取到文件末尾,检查错误 if err := scanner.Err(); err != nil { fmt.Printf("读取文件%s错误: %v\n", filename, err) return } // 短暂休眠避免空循环占用CPU time.Sleep(100 * time.Millisecond) } } } }() return stopChan }
关键细节解释
- 监听目录而非单个文件:原文件被重命名后,旧的文件句柄就失效了,监听目录才能捕获到新文件的创建事件,确保不会错过回滚后的日志。
- 专属停止通道:每个读取goroutine对应一个独立的
stopChan,关闭通道时,goroutine会在select中检测到信号,及时退出,避免goroutine泄漏。 - 文件打开重试逻辑:回滚后新文件可能不会立即创建,加入循环重试确保能成功打开新文件。
- 优雅退出:在goroutine中
defer file.Close(),确保文件句柄被正确释放;停止信号触发后立即return,避免资源浪费。
额外注意事项
- 如果你不需要实时读取到文件末尾的新日志,可以去掉末尾的休眠,改用
fsnotify的Write事件触发读取,但日志回滚场景下主要还是靠目录监听。 - 可以根据业务需求调整重试间隔、等待旧goroutine退出的时间,避免出现竞态条件。
- 生产环境中可以加入更多错误处理逻辑,比如文件权限问题、磁盘满等情况。
内容的提问来源于stack exchange,提问作者R wang
相关产品推荐
相关产品推荐

