go 1.8+ 可直接用 http.server.shutdown() 实现优雅停机,需监听 sigint/sigterm、设置超时、关闭非 http 资源,并处理 websocket 等长连接;endless 适用于热重启但不跨平台;容器中须确保 go 进程为 pid 1 并匹配 terminationgraceperiodseconds。

Go 1.8+ 直接用 http.Server.Shutdown() 就够了
只要你的 Go 版本 ≥ 1.8,根本不需要引入第三方库来实现优雅停机。核心就是把 gin.Engine 包进 http.Server,再监听 SIGINT 或 SIGTERM,调用 Shutdown()。
关键点在于:Shutdown() 会立即拒绝新连接,但允许已接受的连接(包括正在处理的 HTTP 请求)继续运行,直到超时或自然结束。
-
Shutdown()不会等待未建立的连接,只管“已 Accept 但未完成”的连接 - 超时时间必须显式设置,比如
context.WithTimeout(ctx, 5*time.Second),否则可能永久阻塞 - 务必在
Shutdown()前关闭所有非 HTTP 资源(如数据库连接、消息队列客户端),否则Shutdown()返回后进程仍可能卡住 - 别漏掉
err != http.ErrServerClosed判断——ListenAndServe()在正常关闭时会返回这个错误,不该当异常处理
endless 库适合需要热重启(零停机更新)的场景
endless 的本质是 fork 子进程 + 文件描述符继承,旧进程等请求结束后退出,新进程立即接管监听。它解决的是「部署新二进制时不停服务」的问题,不是单纯关机。
它默认响应 SIGHUP(kill -1 $PID)触发重启,SIGINT/SIGTERM 触发关机。注意:
- 子进程启动后,父进程不会立刻退出,而是等所有活跃连接关闭才退出——这点和
Shutdown()行为一致 - 不支持 Windows(依赖 Unix 域套接字和 fork),生产环境若跨平台需绕开
- 日志里看到
Received SIGHUP后出现两个 PID 日志,说明 fork 成功;若只看到一个,可能是权限或环境问题(如容器里没开fork) - 它不处理数据库连接迁移,新进程需自己初始化 DB,旧进程要等事务提交完再关 DB 连接
长连接(如 WebSocket)必须手动管理生命周期
http.Server.Shutdown() 对普通 HTTP 请求有效,但对升级后的 WebSocket 连接无效——因为它们脱离了 HTTP 生命周期,Shutdown() 不感知。
如果你用了 gin.WebSocket() 或直接调 conn.Hijack(),得自己加逻辑:
- 收到关机信号后,向所有活跃 WebSocket 连接发送
close帧,并设标志位禁止新 upgrade - 维护一个
map[*websocket.Conn]bool或用sync.Map记录连接,关机时遍历调Close() - 每个连接 goroutine 内部要监听
ctx.Done(),配合WriteMessage()的超时避免死锁 - 别依赖
defer conn.Close()—— 关机时 goroutine 可能已被取消,defer不执行
容器环境(如 Kubernetes)下信号传递容易被截断
Kubernetes 默认发 SIGTERM 给主进程,但如果你用 sh -c "go run main.go" 启动,信号会发给 shell 而非 Go 进程,导致 signal.Notify() 收不到。
正确做法只有两个:
- Dockerfile 用
ENTRYPOINT ["./your-binary"],确保 Go 进程是 PID 1 - 或用
exec ./your-binary替代sh -c,让 exec 替换当前 shell 进程
另外,K8s 的 terminationGracePeriodSeconds 必须 ≥ 你代码里 Shutdown() 的超时时间,否则 kubelet 会在超时前发 SIGKILL 强杀。











