air默认只监听.go文件,改config.yaml或html无反应;必须在[watch]和[build]两节均配置include_ext=["go","yaml","yml","html","tmpl"],且build.bin与build.cmd路径严格一致,否则启动失败或不重载。

Go 本身不支持热重载,必须靠第三方工具实现;air 是目前最稳定、配置最清晰的选择,但默认行为只监听 .go 文件,绝大多数“没反应”问题都出在监听范围或构建路径配置错误上。
为什么改了 config.yaml 或 templates/xxx.html,air 完全没反应
这不是 bug,是默认配置根本没注册这些文件的 inotify 监听。air 默认只 watch .go 后缀,连 go.mod 都不看,更别说 YAML、HTML、TOML 了。
- 必须在
.air.toml的[watch]和[build]两节都显式声明:include_ext = ["go", "yaml", "yml", "html", "tmpl"] -
include_ext是白名单,不是追加——漏写"go",连main.go都不会触发重建 - 改完配置后必须手动重启
air进程,它不热加载自身配置 - 如果项目结构是
cmd/myapp/main.go,必须在该目录下运行air,否则入口识别失败
build.bin 和 build.cmd 路径不一致导致启动失败
这是 air 启动失败最常卡住的地方:air 按 build.bin 去执行,但 build.cmd 没真把二进制写到那儿,就会报 exec: "./tmp/main": file does not exist。
-
build.cmd = "go build -o ./tmp/main ."→ 输出路径是./tmp/main -
build.bin必须严格等于"./tmp/main",不能是"tmp/main"(少点)或"./tmp/main.exe"(Windows 没配后缀) - Linux/macOS 上确保
./tmp可写,且生成的二进制有可执行权限:build.cmd = "go build -o ./tmp/main . && chmod +x ./tmp/main" - Windows 用户建议用
build.bin = "./tmp/main.exe",对应build.cmd = "go build -o ./tmp/main.exe ."
重载后端口被占、旧进程僵死、ps 找不到进程
本质是 air 发了 SIGTERM,但你的 Go 程序没处理它,旧进程就卡在那儿了,端口实际没释放。
- 必须在
main()中显式捕获os.Interrupt或syscall.SIGTERM,调用http.Server.Shutdown() - 检查
build.delay是否设得太小(比如100),新进程还没起来、旧进程还没关完,端口就被抢 - 临时清理残留:
pkill -f 'tmp/main'(macOS/Linux)或taskkill /F /IM main.exe(Windows) - 别依赖
air init生成的默认配置——它不含kill_delay,而某些框架启动慢,需要留出关闭时间
配置热加载(如 viper)和代码热重载是两回事
air 只负责代码变更后重启进程,它不管配置文件是否生效;配置热加载必须你自己实现,viper.WatchConfig() 只发通知,不自动重读、不自动更新结构体。
- 必须在
viper.OnConfigChange回调里显式调用viper.ReadInConfig()和viper.Unmarshal(&cfg) - 直接赋值全局变量
cfg = newCfg是危险操作:结构体字段多时,goroutine 可能读到部分新、部分旧的值 - 推荐用
atomic.Value存配置指针:globalConf.Store(&newCfg)写入,globalConf.Load().(*Config)读取 - Linux 容器中常见
No space left on device报错——其实是inotify句柄耗尽,需调高fs.inotify.max_user_watches
最容易被忽略的是:air 的监听范围、构建路径、信号处理这三处配置,只要一个没对齐,整个热重载链路就断在中间。它不报错,只是沉默——你得自己盯住日志、ps、端口和文件权限这四个点。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











