go程序收不到sigint或sigterm是因为signal.notify后缺少接收循环;http.server.shutdown需配合带超时的context;多服务关闭应使用sync.waitgroup+共享context协调;测试需压测中发信号并观察日志与端口状态。

Go 程序收不到 SIGINT 或 SIGTERM?检查信号监听是否被阻塞
Go 的信号处理依赖 signal.Notify,但它本身不阻塞,必须配合一个持续运行的接收循环。常见错误是调用完 signal.Notify 就直接退出 main,导致进程瞬间结束,根本来不及响应信号。
典型误写:
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) // 没有后续接收逻辑 —— 程序立刻退出
正确做法是保持主 goroutine 活跃,并在收到信号后触发关闭流程:
- 用
sigChan := make(chan os.Signal, 1)创建带缓冲通道(避免信号丢失) - 务必在
signal.Notify后启动一个for range sigChan或单次阻塞等待 - 如果程序主体是 HTTP server,别把
http.ListenAndServe放在信号监听之后——它会阻塞,导致信号永远收不到
http.Server.Shutdown 调用后立即返回,但连接还在处理?加超时控制
Shutdown 是优雅关闭的核心,但它只是发起通知,不等待完成。若不设超时,可能卡死或无限等待;若超时太短,活跃请求会被粗暴中断。
立即学习“go语言免费学习笔记(深入)”;
关键点:
- 必须传入上下文(
context.WithTimeout),不能用context.Background() - 超时时间建议略大于你最长请求的预期耗时(比如 10–30 秒),而非固定 5 秒
-
Shutdown返回非 nil 错误时,通常意味着上下文已超时或被取消,此时应记录日志,但不必 panic - 调用
Shutdown前,应停止接受新连接(srv.Close不够,它不等已有连接,要用Shutdown)
多个服务共存时,如何统一协调关闭顺序?用 sync.WaitGroup + 共享 context.Context
实际项目常同时跑 HTTP server、gRPC server、后台 worker、数据库连接池等。它们关闭有依赖关系(比如 worker 依赖 DB 连接,DB 连接应在 worker 停止后再关),硬编码顺序易出错。
更可靠的做法是共享一个 cancelable context,并用 sync.WaitGroup 计数:
- 启动每个长期服务时,用
go func() { defer wg.Done(); service.Run(ctx) }() - 收到信号后,调用
cancel(),再wg.Wait()等所有子 goroutine 自行退出 - HTTP/gRPC server 的
Shutdown也应放在这个 context 下,确保它们响应 cancel - 注意:不要在
defer wg.Done()前做阻塞操作(如未设超时的Shutdown),否则wg.Wait会卡住
测试优雅退出是否生效?别只靠 kill -2 手动试
本地手动发信号容易漏掉竞态问题(比如 shutdown 刚开始,新请求又进来了)。真正要验证,得模拟真实压力场景。
实操建议:
- 写一个简单压测脚本,用
ab或hey持续发请求(如hey -z 30s http://localhost:8080/health) - 在压测进行中执行
kill -TERM $(pidof your-binary) - 观察日志:是否打印了 “shutting down…”;是否有请求在 shutdown 开始后仍被处理;是否有请求返回 503 或超时
- 检查
lsof -i :8080是否在 shutdown 完成后彻底清空端口监听
最容易被忽略的是 context 传播深度——比如某层中间件或数据库查询没接收到 cancel,导致 Shutdown 等待超时后强行退出,连接泄漏。这种问题只有压测+日志+端口状态三者交叉比对才能暴露。










