Go 1.8+ 推荐用 http.Server.Shutdown() 实现 Gin 优雅启停,因 r.Run() 阻塞且不响应信号、无法设超时、绕过生命周期管理;须显式创建 http.Server、监听 SIGINT/SIGTERM、用带超时 context 调 Shutdown,并注意 ReleaseMode 设置时机、避免重复中间件、正确忽略 http.ErrServerClosed。

Go 1.8+ 的 http.Server.Shutdown() 是 Gin 服务优雅启动/停止的唯一推荐方式,第三方库如 endless 已过时且不兼容现代 Go 模块与信号处理机制。
为什么不能直接用 r.Run()
r.Run() 是阻塞式调用,内部封装了 http.ListenAndServe(),它既不响应 SIGINT/SIGTERM,也无法控制超时、等待活跃连接关闭。生产环境强行 kill 进程会导致正在处理的请求被中断,客户端收到 connection reset 或空响应。
- 它绕过了
http.Server实例的生命周期管理 - 无法设置
ReadTimeout/WriteTimeout/IdleTimeout - 日志中看不到监听地址变更或关闭过程,调试困难
必须手动构造 http.Server 并调用 Shutdown()
核心逻辑是:显式创建 http.Server,传入 Gin 的 Handler,再用 signal.Notify 监听退出信号,在收到信号后调用 srv.Shutdown() 并传入带超时的 context.Context。
-
srv := &http.Server{Addr: ":8080", Handler: r}——r是*gin.Engine,它实现了http.Handler接口 -
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)—— 必须同时监听两个信号,Linux systemd 发送SIGTERM,本地 Ctrl+C 是SIGINT -
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)—— 超时时间建议设为 5~10 秒;太短会强制断连,太长影响部署效率 - 调用
srv.Shutdown(ctx)后,需defer cancel()防止 context 泄漏
srv := &http.Server{
Addr: cfg.Address,
Handler: r,
}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("server closed unexpectly: %v", err)
}
}()
<!-- 信号监听与 Shutdown -->
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<!-- 阻塞等待信号 -->
<!-- Shutdown 并等待完成 -->
<!-- 日志输出 -->容易忽略的三个细节
这三个点在大多数教程里被跳过,但线上出问题基本都源于此:
-
gin.SetMode(gin.ReleaseMode)必须在gin.New()或gin.Default()之后、注册路由之前调用,否则调试日志仍会输出,且Recovery()中间件行为可能异常 - 若使用
gin.Default(),它已自动注入Logger()和Recovery(),你不需要再r.Use(gin.Logger()),重复调用会导致日志打印两遍 -
srv.ListenAndServe()返回http.ErrServerClosed是正常关闭,不是错误,必须显式忽略,否则会误报 “server closed unexpectly”
真正复杂的不是 Shutdown 本身,而是如何让整个启动流程可配置、可测试、可注入依赖(比如 DB、Redis 客户端)。把 http.Server 构造、信号监听、Shutdown 等逻辑抽成独立函数,比堆在 main() 里更容易覆盖单元测试和集成测试边界。


















