Go本身不支持热重载,所有“热重载”工具本质都是进程级重启:监听文件→杀旧进程→编译新二进制→启动新进程;需严格对齐root、build.bin与build.cmd路径及[watch]/[build]两节include_ext配置。

go 本身不支持热重载,所谓“热重载”全是进程级重启——你看到的“秒更”,其实是 air、hotplex 或自研脚本在背后干了四件事:监听文件 → 杀旧进程 → go build 出新二进制 → 启动新进程。这不是语言特性,是开发期补丁;想靠 go run 实现热重载,等于指望自行车自己上高速。
为什么 go run 不能当热重载用
go run 每次都起全新进程,不管理生命周期,也不释放端口或信号:
- http.Server 没调 Shutdown() 就被 Ctrl+C 中断,下次启动直接报 address already in use
- os.Interrupt 和 syscall.SIGTERM 默认没注册,air 发信号杀进程时,旧实例可能僵死成僵尸态
- 它不区分构建与运行:-gcflags='all=-N -l' 这类调试参数无法注入,Delve attach 会失败
air 配置里最容易踩的三个硬坑
90% 的air 启动失败或静默不生效,问题不在工具本身,而在三处路径/命令没对齐:
- root 必须指向含 go.mod 的目录(通常是项目根),不是 main.go 所在目录;设错会导致 go build 找不到 module
- build.bin 和 build.cmd 输出路径必须严格一致,例如都用 ./tmp/main;不一致会报 exec: "./tmp/main": file does not exist
- [watch] 和 [build] 两节的 include_ext 必须完全相同;只在 watch 里加 yaml,但 build 不认,改配置文件就不会触发重建
改了 internal/handler/xxx.go 没反应?检查监听范围
air 默认只监听当前工作目录及子目录下的 .go 文件,不跨模块、不跟进符号链接、不扫描 vendor/ 或隐藏目录:
- 如果你的 main.go 在 cmd/api/main.go,却在项目根目录运行 air,那 internal/ 下的改动根本不会被看到
- 正确做法:先 cd 到 cmd/api/ 目录下运行 air,或在 .air.toml 中显式配置 watch.cwd = "cmd/api"
- Windows 用户注意:路径一律用正斜杠 /,哪怕在 PowerShell 里写 .\tmp\main 也会导致监听失效
真正难的从来不是“怎么让代码变”,而是“怎么确保所有 goroutine 读到的都是完整、合法、一致的状态”
进程级热重载看似简单,但端口复用、连接中断、状态丢失、信号传递、并发读写这些边界问题,全得靠你手动兜底。比如: -http.Server 必须实现 Shutdown() 并等待活跃连接关闭
- 静态资源变更建议用程序内监听(如 fsnotify 加载模板),而非依赖 air 杀进程
- viper.WatchConfig() 单独调用没反应,是因为它只注册监听器,不阻塞主线程;必须配合 http.ListenAndServe() 或 select{} 让程序长期存活
- 容器环境里 inotify.max_user_watches 默认只有 8192,监听几十个文件就打满,静默失效比报错更危险


















