直接用 r.run() 无法优雅关闭,因其内部调用阻塞式 http.listenandserve() 且不暴露 http.server 实例,无法调用 shutdown();必须手动构造 http.server,监听 sigint/sigterm 信号,用带超时的 context 调用 shutdown(),并单独处理 websocket 连接及资源清理。

直接用 r.Run() 无法优雅关闭
因为 r.Run() 内部调用的是阻塞式 http.ListenAndServe(),不暴露 *http.Server 实例,也就没法调用 Shutdown()。收到 SIGINT 或 SIGTERM 后进程立即退出,正在执行的 5 秒数据库查询、WebSocket 消息推送等请求会被强制中断,客户端看到 connection reset 或空响应。
必须手动构造 *http.Server 并监听信号
核心是把 *gin.Engine 当作 Handler 注入到自建的 http.Server 中,并用 os/signal 监听关机信号:
- 启动服务必须放在 goroutine 中,否则会阻塞主流程
- 用
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)接收标准关机信号(别用SIGUSR1,它常用于日志轮转) -
srv.Shutdown()前要创建带超时的context,比如ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second),并在函数返回前调用cancel() - 不要在
Shutdown()调用后立刻打印 “已退出”——某些 handler 可能还在运行,应确保业务逻辑也检查c.Request.Context().Done()
WebSocket 连接需单独处理
srv.Shutdown() 不会主动断开 WebSocket 长连接,也不会触发 conn.Hijack() 后的读写 goroutine 退出。必须额外维护一个活跃连接池(如 sync.Map),在收到关机信号后遍历发送 websocket.CloseMessage,并设标志禁止新 Upgrade 请求。
生产环境必须用 systemd 托管二进制
nohup go run main.go & 不是守护进程,只是躲过终端关闭,隐患很多:
-
go run每次都临时编译,无调试符号、浪费 CPU、无法加固 - 进程仍属原 shell 会话,未真正脱离 session(
ps -o pid,ppid,sid,tty可验证) - 日志默认写入
nohup.out,无轮转、无权限控制、易撑爆磁盘 - 崩溃后不会自动拉起;systemd 配置需包含
Restart=on-failure、RestartSec=1、ExecReload=/bin/kill -s SIGUSR2 $MAINPID
平滑重启本质是旧进程优雅退出 + 新进程立即拉起,依赖外部信号协作;别再用已归档且不兼容 Go 1.20+ 的 fvbock/endless。











