server.close() 不是优雅关闭,因它立即关闭监听器并中断所有请求;server.shutdown() 才是官方优雅方案:拒新、等旧、释资源,需传带超时的 context。

为什么 server.Close() 不是优雅关闭
它会立刻关闭监听器,所有正在处理的请求被硬中断,客户端收到 connection reset 或空响应,数据库事务、文件写入、消息消费都可能丢一半。这不是“关服务”,是“砸服务器”。server.Shutdown() 才是 Go 1.8+ 官方定义的优雅路径:先拒新连接,再等老请求自然结束,最后释放资源。
-
server.Close()立即返回,不等任何连接;server.Shutdown(ctx)阻塞直到ctx超时或所有连接干净退出 - 必须传带超时的
context.Context,用context.Background()或context.TODO()会导致永远卡住 - 超时时间建议设为 10–25 秒——太短,慢查询或上传来不及完成;太长,K8s 的
terminationGracePeriodSeconds可能已触发强杀
怎么同时捕获 SIGINT 和 SIGTERM
本地开发按 Ctrl+C 发的是 SIGINT;Kubernetes、systemd、Docker stop 发的是 SIGTERM。只监听一个,就等于在一种环境里“优雅”,另一种环境里“直接断电”。
- 用
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)是最小可靠集合,别漏掉syscall.SIGTERM - 通道必须带缓冲:
sigChan := make(chan os.Signal, 1),否则信号可能丢失(尤其快速连按 Ctrl+C 时) - 不要监听
SIGKILL(kill -9),它无法被捕获,也不该处理
连接读取时遇到 n == 0 为什么必须关
conn.Read() 返回 n == 0 且 err == io.EOF,说明对端已发 FIN,连接进入半关闭状态。此时若继续循环读,会立刻返回 0 + nil,CPU 暴涨,连接却一直不释放。
- 错误做法:只检查
err != nil就 break —— 忽略了io.EOF是正常关闭信号,不是错误 - 正确判断条件是:
if n == 0 || err == io.EOF,满足任一就该调conn.Close() - 注意区分:
io.EOF是对方主动关;broken pipe或connection reset by peer是异常断连,同样要关,但需记录日志
长连接(如 WebSocket)为什么 Shutdown() 等不到它
http.Server.Shutdown() 只跟踪 HTTP 连接生命周期,而 WebSocket、SSE、gRPC 流等是通过 Hijack() 或协议升级复用底层 TCP 连接的,net/http 不感知它们是否还活跃。结果就是:你调了 Shutdown(),超时一到就强制退出,这些连接被无声切断。
立即学习“go语言免费学习笔记(深入)”;
- 必须自己维护活跃长连接列表(比如用
sync.Map存*websocket.Conn),收到关闭信号后遍历调.Close() - 每个长连接的读/写 goroutine 都要监听同一个
context.Context,并在后主动清理 - K8s 场景下,还需配合
preStophook 和readinessProbe提前摘流量,否则新请求还在进来,老连接又关不干净
Shutdown(),而是所有环节——HTTP handler 里的上下文传递、数据库连接池的 db.Close()、gRPC server 的 GracefulStop()、第三方消费者(如 sarama)的 Close() 方法、甚至自定义心跳协程——都得响应同一个退出信号。漏一个,进程就卡住。










