router.run()不能用于优雅关机,因其内部直接调用阻塞式http.listenandserve()且不暴露*http.server实例,无法调用shutdown();必须显式创建http.server、goroutine启动、监听信号并合理设置超时与资源清理。

为什么 router.Run() 不能用于优雅关机
因为 router.Run() 内部直接调用阻塞式 http.ListenAndServe(),不暴露 *http.Server 实例,你根本拿不到句柄去调 Shutdown()。本地按 Ctrl+C 或 K8s 发 SIGTERM 后进程秒退,正在执行的 time.Sleep(5 * time.Second)、数据库事务、文件上传都会被硬中断,日志里看不到完成记录,客户端收到 connection reset by peer 或 502。
必须用 http.Server 显式启动并监听信号
核心就三件事:构造 *http.Server、启动服务 goroutine、监听信号后调 Shutdown()。漏掉任一环节,“优雅”就失效。
-
srv := &http.Server{Addr: ":8080", Handler: router, ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second}—— 必须设ReadTimeout和WriteTimeout,否则慢连接会让Shutdown()无限等待 -
go func() { if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatal(err) } }()—— 启动要放 goroutine,否则主线程卡死 -
sigChan := make(chan os.Signal, 1)—— 缓冲大小为 1,防止首次信号丢失 -
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)——SIGQUIT很关键,systemd/k8s 会发它,别只写前两个
Shutdown() 超时和上下文怎么设才合理
Shutdown() 只管 HTTP 连接层,不等你的业务逻辑;它依赖你传入的 context.Context 超时来兜底。设太短会掐断正常请求,太长拖慢发布节奏。
- 电商类接口建议从
context.WithTimeout(ctx, 5 * time.Second)起步;含文件上传或支付回调的,可放宽到 10 秒 - 别在
Shutdown()后立刻os.Exit(),要等 DB/Redis/定时任务等资源清理完再退出 - 验证是否真优雅:启动后立刻
curl -s http://localhost:8080/ping &(后台发一个 5 秒响应),然后马上Ctrl+C,再ps aux | grep yourapp—— 进程消失 + 日志有"Server closed under request"才算过关
哪些 goroutine 容易漏关导致进程卡住
Shutdown() 结束后,main 函数还在等你的业务 goroutine 自行退出。只要有一个没监听 ctx.Done(),进程就卡住不动。
-
time.Ticker:别写go func() { for range ticker.C { ... } }(),改用select { case - 数据库连接池:
defer db.Close()不够,得在 shutdown 阶段显式调db.Close(),否则 GC 不保证及时回收 - Redis 客户端:
redisClient.Close()必须手动触发,别指望 defer - 异步消息发送:
go sendToKafka(...)必须接收 cancel channel,否则发一半就断电
最常被忽略的是健康上报、metrics pusher、后台轮询这些“看起来不重要”的 goroutine —— 它们不响应取消信号,main 就永远等不到返回。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











