Go程序需同时监听os.Interrupt和syscall.SIGTERM以实现优雅终止,http.Server.Shutdown必须配合带超时的context使用,后台goroutine须通过统一done channel响应退出信号。

Go 程序如何监听 os.Interrupt 和 os.Kill 信号
Go 原生不自动处理系统信号,必须显式用 signal.Notify 注册监听。常见误区是只监听 os.Interrupt(对应 Ctrl+C),却忽略 syscall.SIGTERM(Kubernetes、systemd、kill -15 默认发送的信号),导致容器或服务无法优雅终止。
正确做法是同时监听两者,并统一走同一清理流程:
sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) <-- 避免只写 os.Interrupt
注意:syscall.SIGKILL(kill -9)无法被捕获,也不应尝试——它绕过用户态,直接由内核终止进程。
为什么 http.Server.Shutdown 必须配合 context 使用
http.Server.Shutdown 不会自己等待超时,它依赖传入的 context.Context 控制生命周期。若直接传 context.Background(),一旦上下文被取消(比如超时触发),Shutdown 就立即返回,可能中断正在处理的请求。
立即学习“go语言免费学习笔记(深入)”;
典型错误写法:srv.Shutdown(context.Background()) —— 这等价于“立刻关”,没给 HTTP 请求留出完成时间。
推荐方式是用带超时的子 context:
- 设置合理超时(如 10 秒),足够让长尾请求自然结束
- 在 Shutdown 返回后,再关闭 listener、数据库连接等其他资源
- 避免在 Shutdown 期间调用
srv.Close(),会引发 panic
goroutine 泄漏:清理时如何安全等待后台任务退出
很多服务启用了定时器、消息轮询或日志 flush goroutine,它们通常阻塞在 channel 或 sleep 上。若主流程直接退出,这些 goroutine 会残留,造成资源泄漏或数据丢失。
关键原则:所有长期运行的 goroutine 必须响应一个共用的 done channel。
示例结构:
done := make(chan struct{})
go func() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
// 执行健康检查
case <-done:
return // 主动退出
}
}
}()在收到退出信号后,先 close done,再调用 srv.Shutdown,最后 time.Sleep 或 sync.WaitGroup 等待 goroutine 自行退出。
信号处理中容易被忽略的竞态点
多个信号可能快速连续到达(例如反复 Ctrl+C),而 signal.Notify 的 channel 是有缓冲的,若未及时消费,旧信号可能堆积,导致重复执行 shutdown 逻辑甚至 panic。
解决方案很直接:
- 用带缓冲的 channel(容量为 1 即可):
make(chan os.Signal, 1) - 收到信号后立刻
signal.Stop,防止后续信号干扰 - 确保 shutdown 逻辑幂等:多次调用
srv.Shutdown会 panic,需加锁或用sync.Once
最常被跳过的细节是:没有在 main 函数末尾加 os.Exit(0)。如果 shutdown 完成后函数自然返回,程序仍会继续往下执行——可能触发二次初始化或 panic。


















