signal.Notify 必须显式调用并传入 os.Interrupt 和 syscall.SIGTERM,否则无法捕获 Ctrl+C 或 kill -15 信号,导致无法优雅退出;监听需在主循环或服务启动前完成,通道须带缓冲且持续读取。

signal.Notify 必须显式调用,否则 Ctrl+C 或 kill -15 直接杀进程
Go 不拦截任何信号,默认行为就是终止——signal.Notify 不是“可选配置”,而是唯一入口。不调它,os.Interrupt 和 syscall.SIGTERM 都不会进你的程序,defer、日志 flush、连接归还全被跳过。
常见错误写法:signal.Notify(c)(没传信号参数)或只传 os.Interrupt;正确必须写全:signal.Notify(c, os.Interrupt, syscall.SIGTERM)。Kubernetes、systemd、CI 脚本发的几乎全是 SIGTERM,漏掉它等于线上无法优雅退出。
- 监听必须在
http.Serve、srv.ListenAndServe()或长期for循环之前完成,否则信号可能在注册前就到达,被内核静默丢弃 - 别把
signal.Notify放进go func() { ... }()然后让main立即返回——整个进程退出,goroutine 根本没机会调度 -
syscall.SIGKILL(信号 9)永远捕获不到,写了也白写,别加
通道必须带缓冲且持续读取,否则第二次信号就丢了
make(chan os.Signal, 1) 是底线;无缓冲通道在第一次信号到达时就会阻塞主 goroutine,后续信号直接被 Go 运行时丢弃,毫无提示。
更关键的是消费方式:只读一次就退出的 goroutine(比如 go func() { sig := )是典型陷阱。信号不是流数据,设缓冲 >1 反而掩盖设计缺陷——比如本该阻塞等待清理完成,却因缓冲“吞掉”信号而误判流程已结束。
立即学习“go语言免费学习笔记(深入)”;
- 正确做法:用
for { sig := 持续消费 - 缓冲设为 1 已足够,信号是事件通知,不是历史记录
- 不要在 signal handler goroutine 里调
os.Exit(0)——它跳过所有defer,导致 panic: send on closed channel 或连接中断
Shutdown 不是关开关,而是等请求自然结束,超时和依赖治理缺一不可
http.Server.Shutdown 不会强制终止活跃连接,它先关闭 listener,再等正在处理的 handler 返回。如果 handler 里有没设 timeout 的 http.Client.Do、db.QueryRowContext 或未响应 ctx.Done() 的 time.Ticker,就会卡住,最终超时失败并触发强制 Close,返回 500 或事务中断。
- 超时建议设为 10–30 秒:
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) - 数据库安全关闭顺序:
db.SetMaxOpenConns(1)→db.PingContext(ctx)→db.Close(),裸调db.Close()可能丢连接 - 所有外部调用必须显式设 timeout,不能依赖默认值;HTTP handler 必须用传入的
req.Context()做 cancel 控制 - 多个服务共存时,全局只用一个
signal.Notify,用sync.Once包裹 shutdown 流程,避免并发调用 panic
本地测试信号是否真被触发,得靠日志和阻塞验证
按 Ctrl+C 后没看到日志输出?可能根本没走到 signal 处理逻辑——常见干扰包括 panic 恢复机制吞掉错误、select 没监听到 channel、或 goroutine 泄漏导致主 goroutine 提前退出。
- 启动后立刻打印
"signal listener registered",收到信号时打印"received SIGTERM",shutdown 开始时打"starting graceful shutdown",结束打"shutdown completed" - 主 goroutine 不能 return,得用
select {}或sync.WaitGroup卡住,否则进程秒退 - 容器环境注意:
SIGHUP默认不被 Docker 透传,需加--sig-proxy参数;macOS 不支持用户进程用SIGUSR2做平滑重启,开发时建议改用SIGHUP


















