Go 程序应使用 server.Shutdown() 配合带超时的 context 实现优雅退出,而非 os.Exit()、return 或 http.Server.Close();后者会强制中断连接、丢请求、漏清理,而 Shutdown() 先拒新连、再等旧连完成,但需手动设超时并确保所有 handler 和 goroutine 响应 ctx.Done()。

Go 程序不能靠 os.Exit() 或直接 return 来退出,尤其当有 HTTP server、后台 goroutine、数据库连接时——它会跳过清理、丢请求、漏日志、卡住连接。
为什么 http.Server.Close() 是错的
它立刻关闭监听器,并强行中断所有活跃连接。上传中的一百兆文件、正在流式返回的 SSE 响应、长轮询的客户端,全被 TCP RST 终止,前端看到的是 502/ERR_CONNECTION_RESET。
-
Close()不等任何连接完成,也不触发defer或 context 取消 - 它甚至不检查 handler 是否还在用
req.Context()做 I/O,一关了之 - 在 Kubernetes 中,
Close()导致 readiness probe 失败后流量仍被转发进来,瞬间 5xx 爆增
必须用 server.Shutdown() + 超时 context
Shutdown() 是 Go 标准库唯一支持“先拒新连、再等旧连”的方法,但它本身不阻塞,也不自动设超时——你得自己套一层 context.WithTimeout()。
- 永远别传
context.Background():一旦某个 handler 卡在没设ReadTimeout的http.Request.Body.Read(),Shutdown()就永远卡住 - 超时时间建议设为「最长预期请求耗时 + 2~5 秒缓冲」,比如大图生成最多 12 秒,就设 15 秒
-
Shutdown()返回后,要检查 error:如果 err 是context.DeadlineExceeded,说明有连接没及时结束,该记日志但不必 panic
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("graceful shutdown failed: %v", err)
}handler 和中间件必须响应 ctx.Done()
哪怕你用了 Shutdown(),如果 handler 里写了个 time.Sleep(10 * time.Second) 或调了没传 context 的 db.QueryRow(),这个请求照样不会退出,还会拖垮整个 shutdown 流程。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 所有阻塞操作必须用带 context 的版本:
http.Client.Do(req.WithContext(ctx))、db.QueryRowContext(ctx, ...)、redis.Conn.DoWithTimeout(...) - 自定义 long-running goroutine(如上报 metrics)必须在
select中监听,不能只轮询 <code>ctx.Err() != nil - 别在 handler 里启动无 context 控制的 goroutine,比如
go sendToKafka(msg)——它不会因Shutdown()自动停
多个服务共存时,用共享 context.Context + sync.WaitGroup
一个微服务往往不止 HTTP server,还有 gRPC server、消息消费者、定时任务、DB 连接池。它们关闭有依赖顺序,硬编码 shutdown 顺序容易漏或错位。
- 启动时统一用同一个
ctx(来自context.WithCancel())驱动各服务,比如grpcServer.Serve(lis)改为grpcServer.Serve(lis)配合ctx监听取消 - 每个长期服务用
wg.Add(1)后起 goroutine,结尾必defer wg.Done(),且必须放在函数第一行 - 收到信号后调
cancel(),再wg.Wait()—— 注意:不是 defer wg.Wait(),是 main 函数末尾显式调
最容易被忽略的是:shutdown 不是“调个函数就完事”,它是一条链——信号通知 → context 取消 → handler 检查 Done → I/O 库响应取消 → 连接自然关闭 → WaitGroup 计数归零。任一环节断掉,优雅就变成半吊子。尤其是 handler 里那些没加 context 的第三方 SDK 调用,它们就是 shutdown 流程里的“钉子户”。

















