Air在Go微服务中reload失败,主要因默认仅监听.go文件,而微服务依赖的config.yaml、service.proto、templates/*.html等非Go文件修改不触发重启;同时环境变量未透传、proto未联动生成、多服务端口冲突及gRPC优雅关闭缺失,均导致热重载失效。

为什么 Air 在 Go 微服务中经常 reload 失败
Air 默认只监听 .go 文件变化,而微服务项目通常包含大量非 Go 文件:配置文件(config.yaml)、proto 定义(api/v1/service.proto)、模板(templates/*.html)甚至 Docker Compose 文件。一旦这些文件被修改,Air 不会触发重启,导致你改了配置却没生效,误以为代码逻辑有问题。
更隐蔽的问题是:Air 启动的进程默认继承当前 shell 环境变量,但微服务常依赖 ENV=prod、SERVICE_NAME=auth 等变量区分环境。若未显式透传,本地开发时可能加载错误配置,报错如 failed to load config: open config/production.yaml: no such file。
- 用
air -c .air.toml显式指定配置文件,避免隐式行为 - 在
.air.toml中通过env字段声明必需变量,例如:[env] ENV = "dev" SERVICE_NAME = "user"
- 把 proto 生成的
pb/*.pb.go加入build_include,否则改了 proto 但没重新生成,Air 仍用旧代码运行
如何让 Air 正确监听 proto 和 config 变化
Air 的 watcher.include_ext 控制监听哪些后缀,但仅扩展名不够——proto 编译流程涉及 .proto → .pb.go,必须确保 Air 在 .proto 变更后,先执行 protoc 再重启。直接监听 .proto 并不能自动触发生成。
- 把
.proto和.yaml加入watcher.include_ext列表,同时设置build_delay = 1000(毫秒),给 protoc 留出时间 - 在
build.cmd中组合命令:build.cmd = "protoc --go_out=. --go-grpc_out=. api/v1/*.proto && go build -o ./bin/app ./cmd/app"
- 注意路径问题:
protoc命令需在项目根目录执行,否则找不到include或import路径;Air 默认工作目录是启动位置,建议统一用build.run = "./bin/app"并设build.bin = "./bin/app"
多个微服务共存时 Air 进程冲突怎么办
本地跑 user、order、payment 三个服务,每个都用 air -c .air.user.toml 启动,默认都会占用 :8080。Air 自身不管理端口冲突,它只管重启进程,不会检查端口是否已被占用,结果就是 listen tcp :8080: bind: address already in use 报错后静默失败,看起来像“没反应”。
- 每个服务的
.air.toml必须独立,且在build.env中设置不同端口:[build.env] HTTP_PORT = "8001"
,再在代码里读取os.Getenv("HTTP_PORT") - 避免用
air -d(daemon 模式)调试多服务——它会脱离终端,日志难追踪,出错时连 panic 都看不到 - 如果使用
docker-compose up -d配合 Air,记得关掉 compose 里对应服务的command,否则 Air 启动的进程和容器冲突
热重载后 gRPC 客户端连接中断怎么处理
Air 重启时会 kill 原进程,但若服务中启用了 gRPC server,客户端(比如另一个微服务)可能正在发起长连接或流式调用,突然断开会导致 rpc error: code = Unavailable desc = transport is closing。这不是 Air 的 bug,而是信号处理缺失。
- 在 main 函数里用
signal.Notify捕获syscall.SIGUSR2(Air 发送的默认 reload 信号),实现优雅关闭:srv.GracefulStop() // grpc.Server httpServer.Shutdown(ctx) // http.Server
- Air 默认发送
SIGUSR2,不是SIGTERM,别写成监听os.Interrupt - 客户端需实现重试逻辑,比如用
grpc.WithConnectParams设置Backoff,否则第一次请求必失败
真正麻烦的是 context 传递和资源清理——比如数据库连接池、Redis client、后台 goroutine。Air 只负责启动新进程,旧进程的 cleanup 全靠你自己写。漏掉一个 close(ch) 或 cancel(),内存就慢慢涨起来。


















