真正能落地的优雅关闭只有一条路径:http.Server.Shutdown() 配合信号监听和带超时的 context.Context;server.Close() 会立即终止所有连接,不等待请求完成,仅适用于测试,生产环境禁用。

直接调用 server.Close() 或忽略信号直接退出,等于让所有正在处理的请求“断在半路”——上传中断、流式响应被 reset、数据库事务没提交。真正能落地的优雅关闭,只有一条路径:http.Server.Shutdown() 配合信号监听和超时 context。
为什么 server.Close() 不是优雅关闭
server.Close() 会立刻关闭 listener,并强制终止所有已接受但尚未完成的连接。它不等 handler 返回,不等 response 写完,甚至不给 time.Sleep() 一毫秒机会。常见现象包括:
- 客户端收到
connection reset by peer或空响应 - 大文件上传卡在 87% 后失败
- 长轮询接口突然断连,前端反复重试
-
ListenAndServe()立即返回非http.ErrServerClosed错误,日志里满屏 panic
这不是“关得快”,是“关得糙”。Go 官方明确标注:仅用于测试或极端清理场景,生产环境禁用。
必须用 server.Shutdown() 并传入带超时的 context.Context
Shutdown() 是 Go 1.8+ 唯一支持优雅语义的关闭方法,但它本身不阻塞等待——你得自己用 context.WithTimeout() 控制上限,否则可能永远 hang 住。
立即学习“go语言免费学习笔记(深入)”;
- 超时时间建议设为略小于 K8s 的
terminationGracePeriodSeconds(例如后者 30s,这里设 25s) - 必须忽略
http.ErrServerClosed:这是ListenAndServe()正常退出的标志,不是错误 - 别在
Shutdown()前调用server.Close()—— 两者互斥,先Close()就废了Shutdown()的等待逻辑 -
Shutdown()不会主动 cancel handler 中的req.Context(),它只等连接自然结束;所以 handler 自身必须响应上下文取消
最小可靠启动结构:
srv := &http.Server{Addr: ":8080", Handler: myHandler}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()
<p>sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan</p><p>ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("shutdown error: %v", err) // 超时或网络错误才报
}handler 和中间件必须配合上下文取消
Shutdown() 只管连接生命周期,不管 handler 里跑什么。如果 handler 里有:
- 未用
req.Context()的http.Client.Do()请求 → 外部调用卡死,连接无法释放 - 裸
time.Sleep(10 * time.Second)→ 不响应 cancel,超时后被强制 kill - 启用了
go sendToKafka(msg)却没监听ctx.Done()→ goroutine 泄漏 - 数据库查询没传
ctx(如用db.QueryRow()而非db.QueryRowContext(ctx))→ 连接池耗尽
正确写法示例:
http.HandleFunc("/upload", func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
select {
case <-time.After(5 * time.Second):
// 模拟业务处理
case <-ctx.Done():
http.Error(w, "request canceled", http.StatusRequestTimeout)
return
}
<pre class='brush:php;toolbar:false;'>// 所有 IO 必须带 ctx
resp, err := http.DefaultClient.Do(req.WithContext(ctx))
if err != nil && ctx.Err() != nil {
return // 上下文已取消,不处理 err
}})
K8s 和反向代理环境下容易漏掉的三件事
单靠 Go 层 Shutdown() 不足以实现“零请求丢失”:
- K8s 的
readinessProbe必须在收到 SIGTERM 后立即返回非 2xx(如 503),否则 endpoint controller 还没摘除 Pod,新流量就进来了 -
preStop钩子不能做耗时操作(比如等 DB drain),它只适合发一个本地 HTTP 请求触发 Go 服务进入“拒绝新连接”状态 - WebSocket 或 SSE 流式连接不会被
Shutdown()自动感知——它们复用底层 TCP 连接,但net/http不跟踪其内部状态;需在 handler 中显式监听ctx.Done()并 close 底层conn
最易被忽略的一点:没有设置 server.ReadTimeout 和 server.WriteTimeout。一旦某个连接卡在读 header 或写 body,Shutdown() 会无限等待,直到超时 context 强制退出——而这个超时是你唯一可控的兜底机制。


















