air实现热重载本质是进程级重启,需确保root指向含go.mod的根目录、build.cmd与build.bin路径严格一致、include_ext在[watch]和[build]两节均正确配置,否则修改不触发重启或启动失败。

用 air 就够了,其他方案要么配置太重,要么绑定框架、不通用。
air 启动没反应?先确认 root 和 main.go 位置是否匹配
air 默认只监听 root 目录及其子目录下的变更。如果你的 main.go 在 cmd/api/main.go,就必须在该目录下运行 air,不能在项目根目录直接执行——否则改了 internal/handler/user.go 完全不会触发重建。
-
root必须设为含go.mod的路径(通常是项目根目录),不是main.go所在目录;否则go build会找不到 module - 若项目结构是 clean-arch 或多 cmd,建议统一在项目根目录运行
air,并在.air.toml中显式设置root = "." - macOS 上遇到
fork/exec: operation not permitted,大概率是 SIP 隔离了./tmp下生成的二进制,检查该目录是否有执行权限
build.cmd 配错导致静默失败或端口被占
构建命令写错,air 不会报错,而是跳过构建直接杀旧进程——结果就是服务“重启”了但没新进程起来,端口还被僵尸进程占着。
在 Go 中使用 google/wire 实现编译时依赖注入——wire.NewSet、wire.Build、wire.Bind(接口→实现)、wire.Struct、wire.Value、wire.Interface
- 标准模块项目:用
go build -o ./tmp/main .(注意最后的.表示当前 module) - 用了
go.work的多模块 workspace:必须改成go run .,否则go build无法识别 workspace 根 -
tmp_dir要和build.cmd输出路径严格一致,比如设为"./tmp",那命令里就得是-o ./tmp/main - 没写
http.Server.Shutdown()处理SIGTERM,旧进程不会退出,ps aux | grep看不到它,但lsof -i :8080仍显示端口占用
Docker 中热重载要绕过三个典型陷阱
容器内跑 air 不是简单挂代码就行,文件权限、路径解析、信号传递都容易断链。
-
volumes必须挂载整个项目目录(如./:/app),且显式设working_dir: /app,否则go run报no Go files in current directory - Go 服务默认绑定
127.0.0.1:8080,容器内127.0.0.1指向自身,必须改成0.0.0.0:8080才能从宿主机访问 - 用
reflex替代nodemon:Alpine 镜像中执行apk add --no-cache reflex,启动命令写成reflex -r "\.go$" -- sh -c "go run -mod=vendor main.go",正则必须加引号,否则 shell 提前展开 - 调试时暴露
2345端口还不够,Delve 必须用--addr=0.0.0.0:2345,绑127.0.0.1在容器里等于没开
include_ext 配两处,不然改 YAML 或 HTML 不重启
air 默认只监听 .go 文件。想让 config.yaml 或模板变更也触发构建,include_ext 必须在 [watch] 和 [build] 两个 section 都配,缺一不可。
-
[watch]节控制文件监听范围:include_ext = ["go", "yaml", "yml", "html", "tmpl"] -
[build]节决定哪些变更触发构建,内容必须和上面完全一致 - Linux 下
inotify句柄数默认只有 8192,加太多后缀可能触发inotify limit reached,静态资源建议改用程序内fsnotify加载,而非依赖air杀进程 - 编辑器保存延迟(如 VS Code 的
files.autoSaveDelay)设太小(如delay = 200),可能导致air读到半截文件,编译失败
真正卡住人的从来不是工具选错,而是 root、build.cmd、include_ext 这三项没对齐,或者忘了在 main() 里加 Shutdown()。路径、权限、信号处理,三者任一出问题,热重载就变成“看起来在跑,其实没生效”。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










