Gin 本身不提供优雅关闭能力,router.Run() 是阻塞调用且不暴露 http.Server 实例,无法中断;真正起作用的是 http.Server.Shutdown(),需配合 os/signal 监听 SIGINT/SIGTERM、带超时的 context.Context,并显式关闭 time.Ticker、sql.DB、redis.Client 等所有长期 goroutine 和资源,漏掉任一环节进程即卡住。

Gin 本身不提供优雅关闭能力,router.Run() 是阻塞调用,无法中断;真正起作用的是 http.Server.Shutdown(),配合信号监听和 context 超时控制——漏掉任意一个环节(比如没关 time.Ticker、没传 ctx 给 db.QueryRowContext()),进程就会卡住不动。
为什么 router.Run() 不能用于优雅关闭
router.Run() 内部直接调用 http.ListenAndServe(),它会一直阻塞,直到出错或被强杀。你发 SIGINT(Ctrl+C)或 SIGTERM(K8s kill -15),程序立刻退出,正在写的数据库事务没提交、上传的文件只存了一半、SSE 流断在中间……客户端看到的就是 connection reset by peer。
- 必须显式构造
http.Server实例,才能调用Shutdown() -
Shutdown()不会自动等你的业务逻辑结束,只管 HTTP 连接层 - 别在
Shutdown()前调srv.Close()—— 那会立刻断开所有连接,优雅就没了
怎么用 http.Server.Shutdown() 配合信号监听
核心就三件事:启动服务、监听信号、触发关闭。但细节全在 channel 缓冲和 context 超时上。
- 用
make(chan os.Signal, 1)创建带缓冲的 signal channel,否则第一次信号可能丢失 - 只监听
syscall.SIGINT和syscall.SIGTERM,别加SIGHUP等无关信号 -
Shutdown()必须传带超时的context.Context,例如context.WithTimeout(ctx, 10*time.Second),防止卡死 -
Shutdown()返回后不能立刻os.Exit(),要等 DB、Redis、worker 等自定义清理完成
哪些 goroutine 容易被忽略导致进程卡住
HTTP Server 关了,不代表你的程序能退出。下面这些常见组件若没显式关,进程会 hang 住:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
-
time.Ticker:必须在select中监听ctx.Done(),不能只轮询ctx.Err()(会忙等) -
sql.DB:调db.Close(),它会等所有活跃连接归还后再返回 -
redis.Client:调client.Close(),否则底层连接不释放 - HTTP handler 里用了
http.DefaultClient.Do(req)?得改成http.DefaultClient.Do(req.WithContext(ctx))
handler 和中间件必须响应上下文取消
Shutdown() 不会主动 cancel req.Context(),它只等连接自然结束。所以 handler 自身必须透传并响应 ctx:
- 数据库查询必须用
db.QueryRowContext(ctx, ...),不能用db.QueryRow(...) - 第三方 HTTP 调用必须用
client.Do(req.WithContext(ctx)) - 长轮询或流式接口,要在循环中
select监听ctx.Done(),而不是靠time.Sleep() - 中间件里启动的 goroutine(比如日志异步写入),也得接收
ctx并监听ctx.Done()
最常被忽略的不是 Shutdown 本身,而是 handler 里那些没传 ctx 的阻塞调用——它们会让 Shutdown 卡在超时那一刻,却查不出哪条请求没结束。

















