router.Run()不能用于优雅关机,因其是阻塞调用且不暴露http.Server实例,无法传入context或调用Shutdown();必须显式构造http.Server并配合signal监听与context超时调用Shutdown()。

为什么 router.Run() 不能用于优雅关机
因为 router.Run() 是阻塞调用,内部直接调用 http.ListenAndServe(),不暴露 http.Server 实例,也没法传入 context.Context 或调用 Shutdown()。你按 Ctrl+C 或收到 SIGTERM,进程立刻退出,正在执行的 time.Sleep(5 * time.Second)、数据库事务、文件上传都会被粗暴中断,客户端收到 connection reset by peer。
必须显式构造 http.Server 并调用 Shutdown()
Shutdown() 是 Go 1.8+ 提供的真正优雅关机入口,但它只负责 HTTP 连接层:停止接收新请求,等待已有请求完成(最多等你给的超时时间)。它不会自动帮你关掉 time.Ticker、sql.DB、redis.Client、后台 goroutine 等——漏掉任何一个,进程就卡住不动。
实操要点:
- 用
&http.Server{Addr: ":8080", Handler: router}替代router.Run() - 启动服务必须用
go srv.ListenAndServe(),否则主线程阻塞,信号监听无法运行 - 关闭前不要调
srv.Close()—— 那会立刻断开所有连接,优雅就没了 - 必须配合
context.WithTimeout()给Shutdown()设超时,比如 10 秒,避免无限等待
如何监听 SIGINT/SIGTERM 并触发 Shutdown
Go 标准库用 os/signal 监听系统信号,但常见错误是 channel 缓冲区设为 0 或没处理多信号竞争。
正确做法:
- 用
quit := make(chan os.Signal, 1)—— 缓冲为 1,确保第一个信号不丢 -
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM),别加SIGKILL(kill -9无法捕获) - 收到信号后,立即调
srv.Shutdown(ctx),不是srv.Close() - 主 goroutine 最后要等
srv.Shutdown()返回,再做清理(如db.Close())
示例关键片段:
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
<-quit
log.Println("shutting down server...")
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("server shutdown error:", err)
}
log.Println("server exited")
容易被忽略的资源泄漏点
很多人只关了 http.Server,却忘了业务层依赖。进程卡住八成是因为这些没关:
-
sql.DB:必须显式调db.Close(),否则连接池 goroutine 持续运行 -
*redis.Client:调client.Close(),否则底层 net.Conn 不释放 -
time.Ticker:用ticker.Stop(),否则 ticker.C 持续发时间事件 - 自定义 long-running goroutine:需通过
context.Context通知退出,并用sync.WaitGroup等待结束
最危险的是:这些资源不关,Shutdown() 返回后 main 函数结束,但进程仍在后台跑着,K8s 认为它“没死透”,超时后强杀,反而破坏了优雅性。


















