是,iris项目直接用air启动会失败。因air默认发SIGUSR1信号强制kill进程,而iris仅监听SIGINT,导致Shutdown未执行,引发端口占用和goroutine泄漏;需在.air.toml中设send_interrupt=true、kill_delay="2s",并修改代码监听SIGHUP或适配SIGINT。

iris 项目直接用 air 启动会失败?
因为 iris 默认使用 http.Server 的 Shutdown 方法做优雅退出,而 air 在检测到文件变更时会强制 kill 进程并重启——这会导致 Shutdown 没机会执行,进而引发端口被占用、goroutine 泄漏等隐性问题。常见现象是:改完代码保存,air 报 listen tcp :8080: bind: address already in use,或服务看似重启了但旧请求还在处理。
根本原因不是 iris 不兼容热重载,而是它默认的 shutdown 流程和 air 的信号机制没对齐。解决办法不是关掉优雅退出,而是让 iris 主动响应 SIGHUP 或 SIGUSR1(air 默认发送的信号),而不是只等 SIGINT。
- 在
main.go中显式监听SIGHUP,调用app.Shutdown()并立即os.Exit(0) - 确保
app.Run()不阻塞主 goroutine,否则信号无法被捕获 - 避免在
app.Listen()后写阻塞逻辑(比如select{}),否则air发送的信号会被忽略
air 配置文件里必须改哪几项?
air 默认配置对 iris 不友好:它用 SIGUSR1 重启,但 iris 不处理这个信号;它不等待 Shutdown 完成就发 kill;它默认忽略 vendor/ 和 node_modules/,但如果你项目里有自定义中间件目录,也得加进去。
在项目根目录建 .air.toml,关键修改如下:
[misc]
clean_on_exit = true
<p>[build]
cmd = "go build -o ./tmp/main ."
bin = "./tmp/main"
delay = 1000
exclude_dir = ["tmp", "vendor", "assets"]
exclude_file = []
exclude_regex = ["_test\.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
log = "build.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
skip_build = false
use_exec = false</p><p>[dev]
cmd = "./tmp/main"
pids_file = "./tmp/pids"
port = "8080"
host_port = "8080"
reload_delay = 500
colors = true
log = "air.log"
poll = false
poll_interval = 0</p><h1>必须改这里:让 air 发 SIGINT 而不是 SIGUSR1</h1><pre class='brush:php;toolbar:false;'>send_interrupt = true
# 可选:给 Shutdown 留足时间
kill_delay = "2s"</pre>-
send_interrupt = true是核心:强制air发SIGINT,这样iris原生的signal.Notify才能捕获 -
kill_delay = "2s"给app.Shutdown()留出缓冲,防止强制 kill -
exclude_dir加上你实际存放 handler 或 middleware 的子目录,否则改了不触发重载
iris.Shutdown() 怎么写才不卡住?
iris 的 Shutdown() 本身是同步阻塞的,如果 HTTP server 正在处理长连接(比如 WebSocket、流式响应),它会一直等,导致 air 超时后强行 kill。这不是 bug,是设计使然——你得自己控制超时和兜底逻辑。
正确写法是包装一层带 context 的 shutdown,并设合理 deadline:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := app.Shutdown(ctx); err != nil {
app.Logger().Warnf("shutdown error: %v", err)
}
// 强制退出,避免 air 等太久
os.Exit(0)- 别用
time.AfterFunc或select包裹Shutdown,容易漏掉错误判断 - 5 秒是经验值,WebSocket 场景可调到 10 秒;纯 REST API 2 秒足够
- 必须调
os.Exit(0),否则air认为进程没死,下次启动会冲突 - 日志里打
app.Logger().Infof("shutting down, pid: %d", os.Getpid()),方便确认是否真触发了
为什么改了路由或中间件,air 有时不重载?
不是 air 漏检,而是 iris 的注册逻辑发生在 main() 函数内,而 air 只监控源码文件变更。如果你把路由定义抽到单独包里(比如 router/router.go),但没把它加进 exclude_dir 的反向白名单,air 就不会 watch 那个目录。
更隐蔽的问题是:有些开发者用 iris.New() 创建 app 后,又在另一个文件里调 app.Get() ——这种跨文件注册,air 无法感知依赖关系,只能靠你手动告诉它该 watch 哪些路径。
- 运行
air -c .air.toml -d开启 debug 模式,看日志里是否打印 “file changed: xxx.go” - 在
.air.toml的build.exclude_dir删掉你实际存放业务逻辑的目录(比如"internal"、"handlers") - 避免用
go:generate或代码生成工具动态写路由,air对生成文件无感知 - 如果用了
iris.Configurator或iris.Plugins,确保插件初始化也在被监控的文件里
真正麻烦的不是配置本身,而是 iris 的优雅退出和 air 的进程管理之间那几十毫秒的时机差——它不会报错,但会让你反复遇到端口占用、连接 reset、日志断层。盯住 air.log 和应用日志里的 PID 变化,比看文档更管用。


















