必须用 http.Server.Shutdown 配超时 context 才是优雅关闭;Close() 立即关闭 listener,中断所有连接,不等待请求完成。

必须用 http.Server.Shutdown,且必须配超时 context —— 否则服务可能永远关不掉。
为什么 server.Close() 不是优雅关闭?
它会立刻关闭 listener,强制中断所有未完成的连接,正在处理的请求直接丢弃,客户端收到 connection reset 或空响应。这不是“优雅”,是“粗暴终止”。Shutdown() 才是 Go 1.8+ 官方定义的优雅路径:拒绝新连接、等待存量请求自然结束、再释放资源。
-
server.Close()→ 立即返回,不等请求 -
server.Shutdown(ctx)→ 阻塞直到 ctx 超时或所有连接干净退出 - 若 handler 里有死循环(如
select{})、没设超时的 I/O、或阻塞 channel 操作,Shutdown()会卡住
信号监听必须同时捕获 SIGINT 和 SIGTERM
本地开发按 Ctrl+C 发的是 SIGINT;Kubernetes、systemd、Docker 等生产环境发的是 SIGTERM。只监听一个,会导致一种场景下无法触发关闭逻辑。
- 别漏掉
syscall.SIGTERM,尤其上线后常被忽略 - 不用监听
SIGKILL(即kill -9)—— 它无法被捕获,也不该被处理 - 用
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)是最小可靠集合
Shutdown() 的 context 超时时间不是越长越好
设 5 秒还是 30 秒,取决于你最长请求的合理耗时。太短 → 强制中断未完成请求;太长 → 运维等待焦虑,CI/CD 流水线卡住,甚至触发平台级强杀。
立即学习“go语言免费学习笔记(深入)”;
- 推荐从
10 * time.Second起步,上线后根据监控(如 P99 请求时长)调整 - 务必
defer cancel(),避免 context 泄漏 - 错误示例:
srv.Shutdown(context.Background())—— 没超时兜底,一旦有异常请求就永久 hang 住
package mainimport ( "context" "log" "net/http" "os" "os/signal" "syscall" "time" )
func main() { mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r http.Request) { time.Sleep(2 time.Second) // 模拟业务处理 w.Write([]byte("OK")) })
srv := &http.Server{ Addr: ":8080", Handler: mux, } go func() { if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatal(err) } }() quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt, syscall.SIGTERM) zuojiankuohaophpcn-quit log.Println("Received shutdown signal") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { log.Printf("Server shutdown error: %v", err) } log.Println("Server exited gracefully")}
真正难的不是写这几行代码,而是确保每个 handler 都尊重 context(比如用
r.Context()做数据库查询超时)、没有无界 goroutine、第三方库调用也支持取消 —— 这些地方一漏,Shutdown()就形同虚设。










