优雅退出唯一路径是http.Server.Shutdown()配合信号监听与带超时的context.Context,需同步清理数据库、Redis等依赖组件;server.Close()会强制中断连接,Shutdown()无超时则可能永久阻塞。

直接 kill -9 会导致请求丢失、连接重置、事务中断——这不是关闭,是断电。真正能落地的优雅退出,只有一条路径:http.Server.Shutdown() 配合信号监听和带超时的 context.Context,且必须同步清理所有依赖组件。
为什么 server.Close() 不行,而 server.Shutdown() 必须配超时
server.Close() 会立刻关闭 listener 并强制终止所有已接受但未完成的连接,客户端大概率收到 connection reset by peer 或空响应;server.Shutdown() 才是 Go 官方定义的唯一优雅路径,但它本身不设上限——不传带超时的 context.Context,就可能永久卡住(比如 handler 里写了 select{} 或阻塞 channel)。
-
Shutdown()的超时时间建议设为略小于 K8s 的terminationGracePeriodSeconds(例如后者 30s,这里设 25s) - 别用
context.Background()直接调Shutdown(),必须用context.WithTimeout()显式控制上限 -
Shutdown()返回非context.Canceled的 error 才需记录,context.DeadlineExceeded是预期行为,不是故障
必须监听哪些信号,以及怎么注册才不漏
生产环境不能只靠 Ctrl+C 测试:Kubernetes 默认发 SIGTERM,systemd 默认也发 SIGTERM,某些运维脚本或容器平台还会发 SIGQUIT。只监听 os.Interrupt 在线上等于没监听。
- 用
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)覆盖主流场景 - 别加
SIGKILL(kill -9)——它无法被捕获,signal.Notify会静默忽略 -
sigChan缓冲区至少为 1,否则并发发信号可能丢事件 - 监听逻辑必须在
go srv.ListenAndServe()之后、<-sigChan之前执行,顺序错就收不到信号
仅关 HTTP server 远远不够:依赖组件怎么同步退出
Shutdown() 只管 HTTP 连接,但数据库连接池、Redis 客户端、消息消费者、定时任务这些 goroutine 不会自动停——它们会拖住进程,导致 Shutdown() 返回后主 goroutine 仍卡住不退出。
立即学习“go语言免费学习笔记(深入)”;
- 所有长期运行的 goroutine 必须支持
ctx.Done(),比如用time.AfterFunc替代time.Sleep,用select { case 主动退出 - 数据库连接池、gRPC client、Redis client 等必须显式调
.Close()或.Stop(),且要设超时(例如db.Close()前先db.SetConnMaxLifetime(0)加速释放) - 消息消费者(如 Kafka、RabbitMQ)需调
consumer.Close()并等待确认 ACK 发出,否则可能重复消费 - 验证方法:启动服务后发一个慢请求(
curl http://localhost:8080/slow &),再Ctrl+C,用ps aux | grep yourapp看进程是否真退出
handler 和中间件里最容易被忽略的上下文传播点
Shutdown() 不会主动 cancel 请求的 req.Context(),它只等 handler 返回。如果 handler 里调了 db.Query() 而不是 db.QueryContext(ctx, ...),或者用了 http.DefaultClient.Do(req) 而没 req.WithContext(ctx),这个请求就会卡死,拖垮整个 shutdown 流程。
- 所有外部 I/O 操作必须使用带
ctx的版本:db.QueryContext()、redis.Client.GetContext()、http.NewRequestWithContext() - 中间件中启动的异步操作(如日志上报、指标打点)必须监听同一
ctx,否则会持有连接不放 -
http.Server.ReadTimeout和WriteTimeout不影响Shutdown()行为,它们只约束单次读写,不是整个请求生命周期 - WebSocket 或长轮询 handler 更要小心:它们可能维持连接几十秒,
Shutdown()超时后会强制断开,但 handler 内部仍需响应ctx.Done()清理资源
最常被跳过的环节不是代码写法,而是验证——上线前没测过 kill -TERM $(pidof yourapp) 后是否真退出、是否有 goroutine 泄漏、慢请求是否被合理超时切断。信号监听漏一个、超时设错、goroutine 忘关,都会让“优雅”变成“卡死”。


















