Golang初级项目:监控本地文件变动并自动执行命令

2次阅读

用 fsnotify 监控文件变动最简可行路径是:初始化 newwatcher()、add 目录而非文件、显式注册 write/create/rename 事件、另起 goroutine 消费 events/Errors、write 后延迟或监听 rename 判定写完、filepath.abs() 规范路径、windows 启用长路径、退出前关闭 watcher 并清空通道。

Golang初级项目:监控本地文件变动并自动执行命令

用 fsnotify 监控文件变动最简可行路径

Go 里没有内置文件监听能力,fsnotify 是事实标准,但直接上手容易卡在“改了文件却没触发”——根本原因是它默认不递归监听子目录,且对符号链接、重命名等事件类型需显式注册。

实操建议:

  • 初始化时用 fsnotify.NewWatcher(),别用已废弃的 inotify.NewWatcher()
  • 监听目录必须用 watcher.Add("/path/to/dir"),不能只监听单个文件(除非你确定它不会被替换)
  • 必须显式监听 fsnotify.Writefsnotify.Createfsnotify.Rename 三类事件,否则 mv / cp / echo > 后都收不到
  • 启动监听后,务必另起 goroutine 读 watcher.Eventswatcher.Errors,否则会阻塞

执行命令前要检查文件是否写完

linuxWrite 事件可能在文件写入中途就发出,尤其大文件或编辑器(如 VS Code)保存时先清空再写入,直接执行命令会读到空或截断内容。

常见错误现象:cat file.txt | grep something 返回空,但手动查时内容正常。

立即学习go语言免费学习笔记(深入)”;

解决办法:

  • 收到 Write 事件后,不要立刻执行,加一个短延迟(如 100ms)再检查文件修改时间是否稳定 —— 用 os.Stat().ModTime() 对比两次
  • 更稳妥的做法是监听 Rename:很多编辑器保存实际是 tmp → file.txt 的原子重命名,这时才真正“写完”
  • 避免用 exec.Command("sh", "-c", cmdStr) 直接拼字符串,应拆解参数,防止 shell 注入或空格截断

Windows 下要注意路径大小写和长路径限制

fsnotify 在 Windows 上底层用 ReadDirectoryChangesW,对路径大小写不敏感,但 Go 的 os.Statexec.Command 仍按字面路径处理;同时 Windows 默认禁用长路径(>260 字符),而监控目录嵌套深时极易触发。

使用场景:项目在 Windows WSL 和原生环境双跑,但本地测试时突然不触发。

实操建议:

  • 监听前统一用 filepath.Abs() 转为绝对路径,避免相对路径 + 工作目录切换导致失效
  • 确保 Windows 注册表项 HKEY_LOCAL_MACHINESYSTEMCurrentControlSetControlFilesystemLongPathsEnabled 设为 1
  • 避免监听 C:Users... 这类有空格/特殊字符的路径,改用短路径映射(如 subst X: C:longpath)或移到 C:tmp

进程退出时必须关闭 watcher 并等待事件通道清空

漏掉这步会导致程序退出后仍有 goroutine 持有文件句柄,在 Linux 下表现为 lsof -p PID 显示大量 inotify,Windows 下可能报错 The handle is invalid

性能影响:未关闭的 watcher 会持续占用内核 inotify 资源(Linux 默认 8192 个,易耗尽)。

正确做法:

  • defer watcher.Close() 不够 —— 它只关底层句柄,不等事件 channel 消费完
  • 退出前应先 close(watcher.Events)close(watcher.Errors),再从 channel 循环读直到 ok == false
  • 若监听多个目录,watcher.Add() 失败时不会 panic,但后续对该路径的事件将静默丢失,需检查返回 error

最常被忽略的是:在信号捕获(如 os.Interrupt)后直接 os.Exit(0),watcher 根本没机会清理。

text=ZqhQzanResources