Go程序需同时监听os.Interrupt和syscall.SIGTERM以实现优雅关机,因SIGKILL无法捕获;应使用带缓冲的signal.Notify通道、context.WithCancel传递取消信号,HTTP服务调用srv.Shutdown(ctx)并设合理超时(如20秒),清理操作须支持context超时避免goroutine泄漏。

Go 程序如何监听 os.Interrupt 和 os.Kill 信号
Go 本身不自动响应 SIGTERM 或 Ctrl+C,必须显式注册信号监听器。默认情况下,os.Interrupt(对应 Ctrl+C / SIGINT)和 os.Kill(SIGKILL)行为完全不同:前者可捕获并执行清理逻辑,后者**无法被捕获或拦截**,进程会立即终止。所以真正能“优雅关机”的只有 SIGINT、SIGTERM、SIGHUP 这类可捕获信号。
常见错误是只监听 os.Interrupt,忽略容器或 systemd 环境下发的 SIGTERM,导致服务在 Kubernetes 中被强制 kill(超时后触发 SIGKILL)。
- 始终同时监听
os.Interrupt和syscall.SIGTERM - 不要监听
syscall.SIGKILL—— 它会被 Go 忽略,且无任何效果 - 用
signal.Notify时,通道需为带缓冲的chan os.Signal,否则首次信号可能丢失
为什么 context.WithCancel 比全局 flag 更可靠
仅靠信号监听还不够:你得让所有正在运行的 goroutine 主动退出,而不是靠 os.Exit 强制中止。用 context.Context 传递取消信号是最符合 Go 习惯的方式 —— 它天然支持超时、链式取消、跨 goroutine 传播。
容易踩的坑是用一个全局布尔变量(如 isShuttingDown)轮询判断,这既浪费 CPU,又无法及时唤醒阻塞在 net.Listener.Accept 或 time.Sleep 上的 goroutine。
立即学习“go语言免费学习笔记(深入)”;
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
- 在主 goroutine 创建
ctx, cancel := context.WithCancel(context.Background()) - 把
ctx传给 HTTP server、worker pool、数据库连接池等所有长期运行组件 - 收到信号后调用
cancel(),而非直接os.Exit(0) - HTTP server 应使用
srv.Shutdown(ctx),而非srv.Close()(后者不等待活跃请求)
http.Server.Shutdown 的超时设置为何不能太短
srv.Shutdown(ctx) 会尝试 graceful 关闭 HTTP server:停止接受新连接,并等待已有请求完成。但它依赖上下文的 deadline —— 如果 ctx 在 500ms 内就超时,而某个请求还在写响应体,就会被强制中断,返回 503 或连接重置。
真实场景中,下游可能有长轮询、大文件上传、或依赖外部 API 的慢请求。硬编码 5 秒 timeout 在多数服务中偏保守;生产环境建议至少 15–30 秒,并配合健康探针下线逻辑(如先从 LB 摘除实例)。
- 用
context.WithTimeout(ctx, 20*time.Second)构造 shutdown ctx - 在
Shutdown返回 err 后,再调用srv.Close()做兜底(仅当Shutdown超时才需要) - 注意:若 handler 里用了
http.TimeoutHandler,其内部 timeout 可能早于 Shutdown timeout 触发,需对齐
goroutine 泄漏常发生在 defer 清理里
很多人把数据库连接关闭、日志 flush、metrics push 放在 defer 中,但没意识到:如果这些操作本身阻塞(比如网络不可达),而主 goroutine 已因 signal 退出,defer 仍会执行 —— 且没有上下文控制,变成永久阻塞,整个程序 hang 住。
典型表现是:日志显示 “shutting down…”,但进程迟迟不退出,ps 查看仍有 goroutine 处于 select 或 write 状态。
- 所有阻塞型清理操作都应接收 context,例如
db.Close()不行,要用db.CloseWithContext(ctx)(需 driver 支持)或自行加 timeout - 避免在 defer 中启动新 goroutine(如
go flushLogs()),除非它自己管理生命周期 - 用
sync.WaitGroup等待关键清理 goroutine,但 wg.Add() 必须在 goroutine 启动前调用,且需设好超时 fallback
最棘手的部分不是监听信号,而是确保每个子系统都尊重同一个 context 并及时响应取消 —— 尤其是第三方库封装的 long-running client。

















