直接kill-15会导致请求被砍断,因为Go默认对SIGTERM/SIGINT的响应是立即退出进程,不等待HTTP请求返回、数据库事务提交或goroutine自行退出,造成连接重置、日志中断和ESTABLISHED连接残留;必须用http.Server.Shutdown配合context.WithTimeout控制超时,并确保所有goroutine监听ctx.Done()主动退出。

为什么直接 kill -15 会导致请求被砍断
Go 默认对 SIGTERM 和 SIGINT 的响应就是立即退出进程——不等 HTTP 请求返回、不等数据库事务提交、不等 goroutine 自行退出。后果很直观:curl 报 Connection reset by peer,日志最后一行停在 processing req id=abc,netstat 里一堆 ESTABLISHED 连接残留。这不是“退出”,是“猝死”。
http.Server.Shutdown() 必须配超时 context
srv.Shutdown() 本身不设超时,它会一直等所有活跃请求自然结束。如果某个 handler 卡在没设 deadline 的 db.Query() 或 http.Post() 上,服务就永远停不下来。
- 必须用
context.WithTimeout(context.Background(), 30*time.Second),不能用context.Background() - 超时时间建议略大于你最长请求的 P99 延迟(比如文件上传设 30s,普通 API 设 10s)
- 忽略
http.ErrServerClosed,但要记录其他错误,比如context.DeadlineExceeded - 调用前确保
srv.ListenAndServe()已在 goroutine 中启动,否则主线程阻塞,信号监听逻辑压根没机会执行
goroutine 不会自动响应 shutdown,得靠你自己驱动
HTTP 层停了,不代表你的 redis.Client、sql.DB、定时 ticker 或消息消费者就跟着停。它们还在后台跑,main 函数一 return,整个进程就被 OS 强杀,所有 defer 全失效。
- 每个长期运行的 goroutine 启动前调
wg.Add(1),退出前调wg.Done() - 用同一个
context.Context(或stopCh := make(chan struct{}))通知退出,worker 内部必须select { case ,不能只轮询 <code>if ctx.Err() != nil -
sql.DB要先db.SetMaxOpenConns(1)加速连接归还,再db.Close() -
redis.Client必须显式调client.Close();time.Ticker得在select里监听ctx.Done()并调ticker.Stop()
流式 RPC 和 WebSocket 需要手动排空
grpc.Server.GracefulStop() 只关闭 listener、拒绝新请求,**不等 stream 消息发完,也不等 unary RPC 返回**。客户端常收到 transport is closing 错误。
立即学习“go语言免费学习笔记(深入)”;
- 在 gRPC handler 里主动检查
ctx.Done(),收到后尽快清理资源并return - WebSocket / SSE 连接不会被
Shutdown()等待,handler 中必须监听conn.SetReadDeadline()或用select { case - 若 HTTP 和 gRPC 共享端口(如用
http.Serve包裹grpc.Server),要分别调两者的停机方法:先停 gRPC listener,再调http.Server.Shutdown()
最常被忽略的点是:shutdown 流程不是“关服务”,而是“协调退出”。HTTP server、gRPC server、DB 连接池、消息消费者、定时器……每个组件都得自己响应退出信号并完成清理,没人替你兜底。一个环节卡住,整个退出就 hang 住。


















