直接用 r.Run() 无法优雅关机,因其内部调用阻塞式 http.ListenAndServe() 且不暴露 http.Server 实例,导致无法调用 Shutdown();必须手动构造 http.Server 并管理其生命周期。

为什么直接用 r.Run() 无法优雅关机
因为 r.Run() 内部调用了 http.ListenAndServe(),它会阻塞主线程、不暴露 *http.Server 实例,也就没法调用 Shutdown()。一旦收到 SIGINT 或 SIGTERM,进程立即退出,正在处理的请求(比如 5 秒数据库查询、WebSocket 消息推送)会被强制中断,客户端收到 connection reset 或空响应。
必须手动构造 *http.Server,把 *gin.Engine 作为其 Handler,才能控制生命周期。
如何正确封装 Shutdown() 并等待超时
核心是:启动服务用 goroutine,主 goroutine 等待信号,再调 srv.Shutdown() —— 但要注意上下文超时和资源清理顺序。
-
srv.Shutdown()不会自动关闭数据库连接、Redis 客户端或 Kafka 生产者,这些必须手动调用.Close()或.Close()类似方法 - 超时 context 应该在
Shutdown()前创建,并在函数返回前cancel(),否则可能泄漏 goroutine - 若服务依赖 WebSocket,需额外维护活跃连接池(如
sync.Map),关机前遍历发送websocket.CloseMessage,并设标志禁止新Upgrade - 常见错误:在
Shutdown()后立刻log.Println("已退出"),但实际某些 long-running handler 可能还在执行——应确保所有业务逻辑也监听ctx.Done()
平滑重启需要哪些配套机制
Go 本身不支持热 reload 二进制,所谓“平滑重启”本质是:旧进程优雅退出 + 新进程立即拉起。这依赖外部工具或自建信号协作。
关键点:
- 使用
syscall.SIGUSR2触发重启(Linux/macOS),避免和关机信号混淆;SIGUSR1常用于日志轮转,别混用 - 新进程启动前,需检查端口是否就绪(旧进程
Shutdown()完成后才释放端口),否则新进程可能因address already in use启动失败 - 若用 systemd,要配置
Restart=on-failure和RestartSec=1,并配合ExecReload=/bin/kill -s SIGUSR2 $MAINPID - 不要依赖
fvbock/endless:它已归档且不兼容 Go 1.20+ 的net.Listener接口变更;现代做法是用graceful或手写 fork + 文件锁
WebSocket 连接在关机时最容易被忽略
标准 HTTP 请求会在 Shutdown() 阶段自然完成,但 WebSocket 是长连接,srv.Shutdown() 不会主动断开它们,也不会触发 conn.Hijack() 后的读写 goroutine 退出。
必须自己管理:
- 用
sync.Map[string]*websocket.Conn记录所有活跃连接(key 可用 session ID 或 conn.RemoteAddr()) - 收到关机信号后,遍历 map 调
conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseGoingAway, "")) - 每个处理连接的 goroutine 内部必须 select 监听
ctx.Done(),并在退出前从 map 中删除自身 - 否则会出现:服务进程已退出,但客户端还连着,且收不到 close 帧,直到 TCP keepalive 超时(默认数分钟)才断开
Gin 的优雅关机不是加几行 signal 处理就完事,真正难的是把所有异步资源(DB、Redis、Kafka、WebSocket、定时任务)的关闭时机和顺序对齐到同一个 shutdown 流程里。漏掉任意一个,都可能导致内存泄漏、连接堆积或数据丢失。











