fsnotify是首选,因标准库无跨平台文件通知能力,轮询浪费CPU、延迟高且无法感知重命名;fsnotify基于系统API事件驱动、低开销、实时性强,但需手动递归监听子目录并注意符号链接行为。

为什么 fsnotify 是首选,而不是轮询
Go 标准库不提供跨平台文件变更通知,轮询(比如定时 os.Stat)会浪费 CPU、延迟高、且无法感知重命名或移动事件。fsnotify 底层调用系统 API(Linux 的 inotify、macOS 的 kqueue、Windows 的 ReadDirectoryChangesW),事件驱动、低开销、实时性强。但要注意:它默认不递归监听子目录,且对符号链接的处理依赖系统行为。
如何正确监听源码变更并触发重启
核心是捕获 fsnotify.WriteEvent 和 fsnotify.CreateEvent,忽略临时文件和编辑器备份(如 *~、.*.swp、.#*)。重启逻辑不能阻塞监听 goroutine,否则会丢事件。
- 用
err := watcher.Add("cmd/myserver")添加目录,不是单个.go文件 - 启动一个独立 goroutine 处理事件:
for event := range watcher.Events,内部用filepath.Ext(event.Name)过滤非.go文件 - 重启前必须先杀死旧进程:用
oldProc.Signal(os.Interrupt)或oldProc.Kill(),再exec.Command("go", "run", "main.go").Start() - 避免重复重启:加
sync.Mutex或用time.AfterFunc做简单防抖(比如 300ms 内只执行一次)
常见崩溃点:进程未清理干净导致端口占用
直接 cmd.Start() 启动新进程后,旧进程若没退出,net.Listen 会报 address already in use。这不是 Go 错误,而是操作系统级资源冲突。
- 旧进程必须显式终止:保存
*os.Process,调用proc.Signal(syscall.SIGTERM),然后proc.Wait()等待退出(注意超时) - 不要依赖
os.Exit(0)来“退出旧服务”——它只是退出当前进程,而你的监控程序和服务器是两个进程 - 开发阶段可加
net.Listen的ReusePort: true(需 Go 1.19+)临时绕过,但不能替代正确 kill 流程
要不要用现成工具?air 和 reflex 的取舍
自己实现能控制细节(比如跳过 vendor、按包名过滤、集成构建步骤),但 air 已覆盖绝大多数场景,配置即开箱即用。
-
air默认监听.go、.tmpl、.html,支持自定义air.toml中的build.cmd和build.bin - 若项目用了 cgo 或需要预编译,
air的build.flags = ["-tags=dev"]比手写更稳 - 真正需要自研的只有两种情况:要嵌入到已有 CLI 工具中,或需在重启前后执行特定 hook(如数据库迁移检查)
复杂点在于信号传递和子进程状态同步——你得决定是让服务器自己响应 SIGHUP 平滑重启,还是由监控进程完全接管生命周期。后者简单,前者更健壮,但需要服务器代码配合。


















