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

用 fsnotify 监控文件变动最简可行路径
Go 里没有内置文件监听能力,fsnotify 是事实标准,但直接上手容易卡在“改了文件却没触发”——根本原因是它默认不递归监听子目录,且对符号链接、重命名等事件类型需显式注册。
实操建议:
- 初始化时用
fsnotify.NewWatcher(),别用已废弃的inotify.NewWatcher() - 监听目录必须用
watcher.Add("/path/to/dir"),不能只监听单个文件(除非你确定它不会被替换) - 必须显式监听
fsnotify.Write、fsnotify.Create、fsnotify.Rename三类事件,否则 mv / cp / echo > 后都收不到 - 启动监听后,务必另起 goroutine 读
watcher.Events和watcher.Errors,否则会阻塞
执行命令前要检查文件是否写完
Linux 下 Write 事件可能在文件写入中途就发出,尤其大文件或编辑器(如 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.Stat 和 exec.Command 仍按字面路径处理;同时 Windows 默认禁用长路径(>260 字符),而监控目录嵌套深时极易触发。
使用场景:项目在 Windows WSL 和原生环境双跑,但本地测试时突然不触发。
实操建议:
- 监听前统一用
filepath.Abs()转为绝对路径,避免相对路径 + 工作目录切换导致失效 - 确保 Windows 注册表项
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\FileSystem\LongPathsEnabled设为1 - 避免监听
C:\Users\...这类有空格/特殊字符的路径,改用短路径映射(如subst X: C:\long\path)或移到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 根本没机会清理。









