根本原因是shutdown只关闭http服务器本身,不管理手动启动的后台goroutine;需通过ctx.done()主动退出,避免无限循环、无缓冲channel阻塞及未关闭外部资源。

为什么 http.Server.Shutdown 之后还有 goroutine 在跑
根本原因不是 Shutdown 没起作用,而是它只管 HTTP server 本身:关闭监听器、等待活跃连接完成。所有你手动启动的后台 goroutine(比如 metrics 上报、心跳检测、消息轮询)完全不受影响,除非它们自己响应退出信号。
常见泄漏点包括:
- 启动时用
go func() { for { ... } }()启动的无限循环,没监听ctx.Done() - 用了无缓冲 channel 做同步,但写入方提前退出,读取方卡在
上永远等不到 - 数据库连接池、Redis 客户端、gRPC 连接这些资源对象,
Shutdown不会自动调用它们的Close()
如何让自定义 goroutine 主动退出
核心原则:不靠外部 kill,靠内部感知 ctx 取消。任何长期运行的 goroutine 都必须显式检查 ctx.Done(),且不能把 select 写成死循环里只等一个 case。
正确写法示例:
go func(ctx context.Context) {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <p>关键点:</p>
- 传入的
ctx必须是 shutdown 流程中统一 cancel 的那个 - 不能在
select外层套for,否则ctx.Done()只能触发一次退出 - 如果用了
time.Ticker或time.Timer,记得defer ticker.Stop()防止泄露
资源释放顺序为什么不能乱
HTTP 和 gRPC server 是“入口”,DB/Redis/gRPC client 是“出口”。如果先关 DB 连接池,再关 HTTP server,那正在处理的请求在执行 db.Query() 时就会 panic;反过来,如果 HTTP 已停但 Redis client 还在发心跳,服务注册中心可能误判实例仍存活。
推荐串行顺序(带超时兜底):
- 先调
grpcServer.GracefulStop()(拒绝新请求,等旧请求结束) - 再调
httpServer.Shutdown(shutdownCtx)(同理,需传入带超时的context.Context) - 最后依次调
db.Close()、redisClient.Close()、grpcConn.Close()
注意:GracefulStop() 本身不设超时,必须靠外层 context.WithTimeout 控制整体时限;Shutdown() 在超时后会强制中断未完成连接。
为什么加了 sync.WaitGroup 还是等不到 goroutine 结束
最常踩的坑是 WaitGroup.Add() 和 Done() 不配对,或者 Wait() 调得太早——比如在 signal 收到前就调了 wg.Wait(),主线程直接卡住,根本收不到信号。
安全做法:
-
wg.Add(1)必须在go func()启动前调用 -
wg.Done()必须放在 goroutine 最末尾,或defer中 -
wg.Wait()必须放在所有Shutdown调用之后,且要加超时保护(用select+time.After)
否则,某个 goroutine 因逻辑 bug 卡在 channel 或网络调用上,整个 shutdown 流程就 hang 死了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











