必须用 http.Server.Shutdown() 而非 server.Close():前者优雅关闭 HTTP 连接,后者强制中断;需配合带 deadline 的 context、信号监听与手动清理 DB/Redis/WebSocket 等资源。
必须用 http.Server.Shutdown(),别碰 server.Close()
server.close() 是底层 socket 级强制关闭:监听器立刻失效,所有活跃连接被 connection reset 中断。大文件上传、sse 流、长轮询全会断在半路。它不等任何请求完成,也不管你有没有事务要提交。
http.Server.Shutdown() 才是标准解法:先拒新连(关闭 listener),再逐个等待存量 HTTP 连接自然返回或超时。但它只管 HTTP 层——WebSocket、gRPC、TCP 长连接不在其管辖范围内,得你手动处理。
- 调用前建议先显式
ln.Close()(若你用了自定义 listener),避免信号接收间隙涌入新连接 - 必须传入带 deadline 的
context.Context,例如context.WithTimeout(ctx, 15*time.Second) - 不能传
context.Background(),否则可能永久阻塞,K8s terminationGracePeriodSeconds 到期后直接强杀
信号监听必须带缓冲 + 显式接收,否则第一次 SIGTERM 就丢
Go 不自动响应信号。signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) 只是注册,不阻塞也不触发逻辑。常见错误是注册完就启动 srv.ListenAndServe(),结果主 goroutine 被阻塞,信号永远收不到。
- 通道必须带缓冲:
sigChan := make(chan os.Signal, 1),否则并发发多个信号时会丢失 - 必须有显式接收动作,比如
<-sigChan或for range sigChan,且该逻辑不能被其他阻塞操作挡住 - 推荐把
srv.ListenAndServe()放到 goroutine 里,留出主 goroutine 专门等信号
Shutdown 后进程还卡住?检查这些常被漏关的组件
http.Server.Shutdown() 返回 ≠ 程序能退出。DB 连接池、Redis 客户端、Kafka consumer、time.Ticker 等若没显式关闭或未响应 ctx.Done(),goroutine 会持续运行,进程 hang 住。
-
sql.DB:调db.Close()前,建议先db.SetMaxOpenConns(1)加速连接归还,再db.PingContext(ctx)确认空闲 -
redis.Client(v9):必须调client.Close(),它会等 pending 命令完成 -
time.Ticker:不能只轮询ctx.Err()(忙等),得在select中监听ctx.Done()并ticker.Stop() - 后台 consumer(如 Sarama):需调
consumer.Close(),并确保没有 goroutine 在持续Consume()
长连接(WebSocket / SSE)不会被 Shutdown 自动等待
http.Server.Shutdown() 对已升级的 WebSocket 或 text/event-stream 响应完全无感知——它们复用 TCP 连接,但 net/http 不跟踪其内部状态。结果就是:Shutdown 超时强制退出,客户端连接突然中断。
立即学习“go语言免费学习笔记(深入)”;
- 必须手动维护连接池:handler 中把
conn存进sync.Map,收到 shutdown 信号后遍历调conn.Close() - 更稳妥的做法是在 preStop hook 或 readinessProbe 切换后,主动向客户端发 disconnect 指令,再等几秒才触发 Shutdown
- 所有 handler 内部的阻塞 I/O(DB 查询、HTTP 调用)必须用
Context控制,比如db.QueryRowContext(ctx, ...)
time.Ticker,或是没设 timeout 的 DB 查询,又或者 WebSocket 连接池根本没存 —— 它们不会报错,只会让进程静默 hang 住。


















