go服务需监听sigterm/sigint信号,调用http.server.shutdown()配合超时context,并同步关闭db、mq等资源;三者缺一不可,且handler必须响应ctx.done()。

Go 服务启动后如何响应 SIGTERM 并执行清理?
Go 程序无法靠 os.Exit() 或直接 return 主函数来实现优雅退出——它会立刻终止所有 goroutine,跳过 defer、关闭监听器、释放资源等关键步骤。真正可行的方式是监听系统信号(主要是 SIGTERM 和 SIGINT),在收到信号后主动触发 shutdown 流程。
核心思路:用 signal.Notify() 捕获信号,配合 context.WithTimeout() 控制 shutdown 超时,再逐个关闭 HTTP server、DB 连接池、自定义资源等。
- 不要只监听
SIGINT(Ctrl+C)而忽略SIGTERM(k8s/kubectl delete、systemd stop 默认发的信号) - 避免在 signal handler 里直接调用
http.Server.Shutdown()—— 它是阻塞的,应放到单独 goroutine 中执行 - 务必设置 shutdown 超时(比如 10 秒),防止某个资源卡死导致进程永远不退出
http.Server.Shutdown() 必须搭配 context 使用吗?
必须。虽然 http.Server.Shutdown() 接收一个 context.Context 参数,但它不是“可选增强”,而是控制等待行为的唯一机制:该 context 决定最多等多久让正在处理的请求完成,超时后强制关闭连接。
常见错误是传入 context.Background() 或 context.TODO(),这会让 Shutdown() 无限期等待——哪怕所有 handler 都已返回,只要底层 TCP 连接还没自然断开(如 keep-alive),它就不返回。
- 正确做法:用
context.WithTimeout(ctx, 10*time.Second),并在 defer 中调用cancel() - 如果 handler 内部用了 long-polling 或 streaming(如 SSE),需确保它们能响应 context Done() 信号并提前退出
-
http.Server.Close()是粗暴关闭,会立即中断所有连接,不能替代Shutdown()
如何统一管理多个需要关闭的资源?
除了 http.Server,你还可能有数据库连接池(*sql.DB)、消息队列消费者(如 kafka.Consumer)、自定义的后台 goroutine(如定时刷新缓存)等。它们没有统一接口,但可以抽象为「可关闭资源」。
推荐用切片保存关闭函数,在 shutdown 阶段顺序调用:
var cleanupFuncs []func() error
// 注册 DB 关闭
cleanupFuncs = append(cleanupFuncs, func() error {
return db.Close()
})
// 注册 Kafka 消费者关闭
cleanupFuncs = append(cleanupFuncs, func() error {
return consumer.Close()
})
// 执行清理
for _, f := range cleanupFuncs {
if err := f(); err != nil {
log.Printf("cleanup failed: %v", err)
}
}
- 注意关闭顺序:先停接收新请求(如关 HTTP server),再关下游依赖(如 DB、MQ)
- 每个关闭函数应尽量幂等,允许重复调用不 panic
- 避免在 cleanup 函数中阻塞太长时间;如有必要,内部也加超时
为什么 defer http.Server.Shutdown() 不起作用?
因为 defer 只在函数返回时执行,而 main 函数通常不会自然返回——它会一直运行直到被 kill。所以把 srv.Shutdown() 写在 main 函数末尾并用 defer 包裹,等于永远不会执行。
真正生效的 shutdown 必须由外部事件触发(信号),并在事件处理路径中显式调用。典型结构是:signal.Notify() → 启动 goroutine 执行 srv.Shutdown() + 清理 → os.Exit(0)。
- 别依赖
os.Interrupt常量去比对syscall.Signal值,用signal.Notify(ch, os.Interrupt, syscall.SIGTERM)更安全 - main 函数最后要
select{}阻塞住,否则程序启动完就退出了 - shutdown 过程中若发生 panic,会导致整个进程 crash,应在关键 cleanup 步骤外层加 recover
context.Context**。哪怕 shutdown 流程写得再完整,只要某个 handler 在数据库查询或 HTTP 调用时没把 context 传下去,它就会卡在 I/O 上,拖垮整个 graceful shutdown。











