根本原因是未正确配置signal.Notify:必须用带缓冲的channel(如make(chan os.Signal, 1))并显式注册syscall.SIGTERM和syscall.SIGINT,否则信号被丢弃或无法捕获;同时需确保程序运行在PID 1且环境透传信号。

Go 里用 signal.Notify 捕获 SIGTERM/SIGINT 为什么收不到信号?
常见现象是程序启动后按 Ctrl+C 没反应,或容器中发 kill -15 后进程立刻退出、没执行清理逻辑。根本原因通常是:signal.Notify 没绑定到正确的 channel,或者 channel 容量不足导致信号被丢弃。
必须用带缓冲的 channel(哪怕只缓存 1 个),否则第一个信号就可能阻塞或丢失:
// ✅ 正确:至少缓冲 1 sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT) <p>// ❌ 错误:无缓冲 channel 在未及时接收时会丢信号 sigChan := make(chan os.Signal) signal.Notify(sigChan, syscall.SIGTERM)
- 始终传入具体信号值,不要依赖
os.Interrupt或os.Kill—— 后者在 Linux 上无法捕获(os.Kill是强制终止,内核不投递) - 避免在
signal.Notify之后才创建 channel,顺序错误会导致漏信号 - 多个 goroutine 同时监听同一 channel 是安全的,但仅需一个 goroutine 负责接收和响应
如何保证 http.Server.Shutdown 真正等完所有请求?
http.Server.Shutdown 不会自动等待长连接(如 WebSocket、流式响应、慢客户端)完成,如果直接调用后立刻退出,活跃连接会被强制断开。
关键点在于设置合理的超时,并主动关闭 listener:
立即学习“go语言免费学习笔记(深入)”;
srv := &http.Server{Addr: ":8080", Handler: mux}
go func() {
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}()
<p><-sigChan // 等信号
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()</p><p>// 先关闭 listener,阻止新连接
if err := srv.Close(); err != nil {
log.Printf("close listener failed: %v", err)
}</p><p>// 再触发 Shutdown,它只处理已有连接
if err := srv.Shutdown(ctx); err != nil {
log.Printf("shutdown failed: %v", err)
}-
srv.Close()必须在Shutdown()前调用,否则新连接仍可建立 - 超时时间要大于最长业务处理时间,否则会强制中断;但也不宜过长(如设成 5 分钟),影响部署节奏
-
Shutdown返回context.DeadlineExceeded表示超时,此时应记录日志并继续释放其他资源
多个资源(DB、Redis、gRPC client)怎么统一释放且不阻塞退出?
不能把所有 Close() 都串行写在主 goroutine 里——某个资源关闭卡住(比如网络抖动下 Redis Close() 超时),整个退出流程就挂起。
推荐用带超时的并发关闭 + 等待组:
var wg sync.WaitGroup
done := make(chan struct{})
<p>wg.Add(3)
go func() { defer wg.Done(); db.Close() }()
go func() { defer wg.Done(); redisClient.Close() }()
go func() { defer wg.Done(); grpcConn.Close() }()</p><p>go func() {
wg.Wait()
close(done)
}()</p><p>select {
case <-done:
case <-time.After(5 * time.Second):
log.Warn("some resources timed out on shutdown")
}- 每个资源关闭都包裹在独立 goroutine 中,避免单点失败拖垮整体
- 用
sync.WaitGroup+select控制最大等待时间,比单纯time.Sleep更可靠 - 注意:像
*sql.DB的Close()实际上只是关闭连接池,通常很快;但某些 SDK(如旧版 etcd client)的Close()可能阻塞,务必查文档
容器环境下 signal.Notify 收不到 SIGTERM 怎么排查?
典型表现:Kubernetes Pod 删除时,容器进程直接被 KILL(信号 9),而不是先收到 TERM(信号 15)。这说明应用没在 PID 1 进程里运行,或镜像入口配置不当。
检查三件事:
- 确认 Go 程序是容器的 PID 1:运行
ps -eo pid,comm | head,第一行应为你的二进制名,不是sh或bash - Dockerfile 中避免用
shell form(如CMD ["./app"]✅,而非CMD ./app❌),后者会启动 shell 作为 PID 1,信号无法透传 - Kubernetes 中确保未设置
terminationGracePeriodSeconds: 0,且容器未被 OOMKilled(此时跳过 SIGTERM 直接 KILL)
最简验证方式:进容器执行 kill -15 1,看程序是否触发退出逻辑——如果不是,问题一定出在信号传递链路上。


















