必须用http.Server.Shutdown()配合带超时的context.Context,否则不是优雅关机而是直接杀进程;server.Close()立即中断所有连接导致上传中断、流响应RST,os.Exit()跳过defer、日志和事务清理;信号监听需带缓冲通道并显式接收,Shutdown超时须小于K8s terminationGracePeriodSeconds且手动清理gRPC、DB、WebSocket等资源。

必须用 http.Server.Shutdown() 配合带超时的 context.Context,否则不是优雅关机,是直接杀进程。
为什么不能用 server.Close() 或 os.Exit()
server.Close() 会立刻关闭 listener 并强制中断所有活跃连接——大文件上传断在 73%,SSE 流被 RST,客户端收到 Connection reset by peer。它不等 handler 返回,也不触发任何 context 取消逻辑。os.Exit() 更糟:defer 不执行、DB 连接不归还、日志缓冲没 flush、事务可能半途回滚。这两者都绕过了 Go 标准库唯一支持的退出路径:http.Server.Shutdown()。
信号监听必须带缓冲且显式接收
常见错误是只调 signal.Notify(sigChan, syscall.SIGTERM) 就启动服务,结果主 goroutine 被 srv.ListenAndServe() 卡住,信号永远收不到。正确做法是:
-
sigChan := make(chan os.Signal, 1)—— 缓冲为 1,防 SIGTERM 连发丢失 - 明确监听
syscall.SIGINT和syscall.SIGTERM,别用os.Interrupt(Windows 上行为不一致) - 把
srv.ListenAndServe()放到 goroutine 里,留出主线程阻塞读sigChan
Shutdown() 必须配 context.WithTimeout(),且超时时间要对齐实际场景
http.Server.Shutdown() 本身是非阻塞的,它只发通知、不等完成。传 context.Background() 等于没设限,一旦某个 handler 卡在没设超时的 http.Request.Body.Read() 或 db.QueryRow() 上,整个 shutdown 就 hang 住。关键点:
立即学习“go语言免费学习笔记(深入)”;
- 超时值必须略大于你最长请求预期耗时(如文件上传需 20 秒,就设
25 * time.Second) - K8s 环境下,必须比
terminationGracePeriodSeconds小 5–10 秒(比如后者设 30 秒,Go 里就设 20 秒) - 错误检查要忽略
http.ErrServerClosed,它是ListenAndServe()正常返回的信号,不是异常
HTTP 层之外的资源必须手动清理,且顺序和响应性决定是否真“优雅”
http.Server.Shutdown() 只管 HTTP 连接,gRPC、DB、Redis、WebSocket、后台 goroutine 全得你亲手关。漏一个,进程就卡住:
- gRPC server 用
grpcServer.GracefulStop(),不是Stop() -
*sql.DB先调db.SetMaxOpenConns(1)加速连接归还,再db.Close() - WebSocket 连接要存进
sync.Map,shutdown 时遍历调conn.Close() - 所有长期 goroutine 必须用
select { case 监听退出,不能靠 <code>time.Sleep()或轮询ctx.Err()
真正难的不是写那行 srv.Shutdown(ctx),而是让每个 handler、每个中间件、每个第三方 client 都尊重同一个 ctx.Done() —— 否则 shutdown 就是假动作。


















