必须用 http.Server.Shutdown() 配合信号监听才能安全退出,因为 r.Run() 内部调用 http.ListenAndServe() 会在收到 SIGINT/SIGTERM 时立即退出,不等待处理中的请求完成,导致连接重置、事务中断、服务未注销等问题;Shutdown() 则能拒绝新连接并等待活跃连接超时或完成。

必须用 http.Server.Shutdown() 配合信号监听,否则直接 kill 或 Ctrl+C 会中断正在处理的请求。
为什么不能只调用 r.Run() 就完事?
r.Run() 是 Gin 封装的快捷启动方式,它内部调用的是 http.ListenAndServe(),而这个函数在收到 SIGINT/SIGTERM 时会立即返回错误并退出 —— 完全不等待已有连接完成。这意味着:
- 客户端可能收到
connection reset或空响应 - 数据库事务、文件写入、日志 flush 等清理动作根本没机会执行
- 微服务注册中心无法及时 deregister,下游还在往已退出节点发请求
怎么用 http.Server.Shutdown() 实现真正安全退出?
核心是:自己构造 http.Server,拿到 listener 控制权,并在收到信号后调用 Shutdown() —— 它会阻塞直到所有活跃连接关闭或超时。
-
Shutdown()不接受新连接,但允许已 Accept 的连接继续处理 - 必须传入带超时的
context.Context,否则可能无限阻塞(比如某个请求卡死) - 需手动监听
os.Interrupt和syscall.SIGTERM,Linux 容器常用SIGTERM - 清理逻辑(如 DB close、注销服务)要放在
Shutdown()调用之后、log.Fatal()之前
示例关键片段:
srv := &http.Server{
Addr: ":8000",
Handler: router,
}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
<-quit
log.Println("shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("server shutdown error:", err)
}
// 此处可加 db.Close()、deregister() 等
log.Println("server exited")
容易被忽略的三个坑
实际部署中,这几个点最容易导致“看似优雅、实则丢请求”:
- 没设
context.WithTimeout():若某请求 hang 住,Shutdown()永远不返回,进程卡死 - 在
Shutdown()前就 close 了 DB 或连接池:后续请求 handler 里执行db.Query()会 panic - 用
signal.Notify(quit, os.Interrupt)却没加syscall.SIGTERM:Kuberneteskubectl delete pod发的是 SIGTERM,不是 Ctrl+C 的 SIGINT
真正安全的退出,本质是控制「新连接准入」和「旧连接生命周期」两个开关,而 Shutdown() 是唯一同时管住这两者的标准方式。别绕开它去自己维护 conn 计数或 sleep 等待 —— 那些都不可靠。


















