router.run()会阻塞资源释放,因其内部无限循环监听http请求,导致defer和main结尾的清理逻辑无法执行;必须手动构建http.server并调用shutdown()配合信号捕获与超时控制,同时显式关闭数据库、日志、缓存及第三方客户端等所有资源。

为什么 router.Run() 会阻塞资源释放
直接调用 router.Run(":8080") 启动服务后,程序会永久阻塞在监听循环里,defer 和 main() 结尾的清理逻辑根本不会执行。数据库连接、日志句柄、缓存池这些资源就一直挂着,直到进程被强杀 —— 这不是“释放”,是泄漏。
必须显式调用 http.Server.Shutdown()
Go 1.8+ 提供的 Shutdown() 是唯一标准路径:它先关闭监听器,再等待已有连接完成处理,最后释放底层 socket 和协程。但 Gin 没有暴露内部 *http.Server 实例,所以不能直接用。
正确做法是自己构建 *http.Server,把 Gin 的 Engine 作为 handler 传进去:
srv := &http.Server{
Addr: ":8080",
Handler: router,
}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()
// 收到信号后
if err := srv.Shutdown(context.Background()); err != nil {
log.Fatal(err) // 等待超时或出错
}
注意:Shutdown() 不会自动关闭数据库连接或日志文件,它只管 HTTP 层。
数据库、日志、缓存等资源要手动释放
HTTP 服务停了,不代表其他资源自动收摊。常见遗漏点:
-
sql.DB必须显式调用db.Close(),否则连接池一直存活 - 如果用了
logrus或zap,需调用logger.Sync()刷盘并关闭文件句柄 -
sync.Pool不需要手动清空,但若复用了带状态的对象(比如自定义 buffer),应在Pool.New中重置 - Redis 客户端、gRPC 连接池等第三方 client,都有自己的
Close()方法,必须调用
信号捕获和超时控制容易被忽略
没设超时的 Shutdown() 可能永远卡住 —— 比如某个请求卡在外部 API 调用上。实际部署中必须加 context 超时:
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("shutdown timeout: %v", err)
os.Exit(1)
}
同时要监听 SIGINT 和 SIGTERM,不能只靠 Ctrl+C:
sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
真正难的不是写几行 shutdown 代码,而是理清所有资源的生命周期边界 —— HTTP server、DB、logger、cache、第三方 client,每个都得单独关,顺序还不能错。漏掉一个,进程就变成僵尸。











