You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何重复启停Goroutine以应对日志文件轮转问题?

解决Golang日志回滚时的文件读取goroutine启停问题

嘿,同为刚入门Golang的新手,太懂你遇到日志回滚这种问题时的头大了!我之前也踩过几乎一模一样的坑,给你分享个亲测有效的实现思路,核心是用信号通道来精准控制goroutine的启停,配合文件系统监听工具(你提到的ExampleNewWatcher应该就是fsnotify库的示例吧?这个库非常适合做文件变化监听)。

核心思路

日志回滚的本质是:原chats.log被重命名/移走,新的chats.log被创建。我们需要:

  1. 监听目标文件所在的目录(而不是单个文件),捕获文件的创建、删除、重命名事件
  2. 用一个专属的停止通道(stopChan)控制每个读取goroutine的生命周期
  3. 检测到回滚事件时,先关闭旧的停止通道让旧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
}

关键细节解释

  1. 监听目录而非单个文件:原文件被重命名后,旧的文件句柄就失效了,监听目录才能捕获到新文件的创建事件,确保不会错过回滚后的日志。
  2. 专属停止通道:每个读取goroutine对应一个独立的stopChan,关闭通道时,goroutine会在select中检测到信号,及时退出,避免goroutine泄漏。
  3. 文件打开重试逻辑:回滚后新文件可能不会立即创建,加入循环重试确保能成功打开新文件。
  4. 优雅退出:在goroutine中defer file.Close(),确保文件句柄被正确释放;停止信号触发后立即return,避免资源浪费。

额外注意事项

  • 如果你不需要实时读取到文件末尾的新日志,可以去掉末尾的休眠,改用fsnotify的Write事件触发读取,但日志回滚场景下主要还是靠目录监听。
  • 可以根据业务需求调整重试间隔、等待旧goroutine退出的时间,避免出现竞态条件。
  • 生产环境中可以加入更多错误处理逻辑,比如文件权限问题、磁盘满等情况。

内容的提问来源于stack exchange,提问作者R wang

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 07:17:52