正确写法是 signal.Notify(c, os.Interrupt, syscall.SIGTERM) 且通道需缓冲(如 make(chan os.Signal, 1)),否则漏收 SIGTERM 导致无法优雅退出,或因无缓冲通道阻塞而丢弃后续信号。

Go 程序不自动处理任何系统信号,signal.Notify 是唯一入口;漏掉 syscall.SIGTERM 或通道没缓冲,都会导致线上服务被强制 kill 后残留连接、丢日志、事务中断。
signal.Notify 必须显式传入 os.Interrupt 和 syscall.SIGTERM
只监听 os.Interrupt(即 SIGINT)是常见错误。Kubernetes、systemd、CI 脚本发的几乎全是 SIGTERM,漏掉它等于放弃优雅退出能力。
-
signal.Notify(c)不传信号参数 → 捕获所有信号,包括不可控的SIGKILL(实际无效)和调试用的SIGUSR1,反而掩盖问题 -
signal.Notify(c, os.Interrupt)→ 收不到kill -15,容器平台反复重试后直接发SIGKILL - 正确写法必须是:
signal.Notify(c, os.Interrupt, syscall.SIGTERM) -
syscall.SIGKILL永远捕获不到,加进去纯属误导自己
通道必须带缓冲且主 goroutine 持续读取
无缓冲通道在第一次信号到达时就阻塞主 goroutine,后续信号被 Go 运行时静默丢弃——你按两次 Ctrl+C,程序只响应一次,且毫无日志或报错提示。
- 缓冲大小设为 1 是底线:
make(chan os.Signal, 1) - 不能只读一次就退出:
go func() { sig := 是典型陷阱,第二次信号永远进不来 - 必须用
for { sig := 或更稳妥的 <code>for { select { case sig := - 缓冲设 >1(如 10)会掩盖设计缺陷:比如本该阻塞等待清理完成,却因缓冲“吞掉”信号而误判流程已结束
Shutdown 必须配合带超时的 context,不能用 context.Background()
http.Server.Shutdown 不是关开关,它先关闭 listener,再等 handler 自然返回。若 handler 里有没设 timeout 的 http.Client.Do 或未响应 ctx.Done() 的 time.Ticker,就会卡住,最终超时失败并触发强制 Close。
立即学习“go语言免费学习笔记(深入)”;
- 错误写法:
srv.Shutdown(context.Background())→ 等价于立刻关,没给请求留出完成时间 - 推荐写法:
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second); defer cancel(); srv.Shutdown(ctx) - 数据库安全关闭顺序:
db.SetMaxOpenConns(1)→db.PingContext(ctx)→db.Close();裸调db.Close()可能丢连接 - 所有外部调用(HTTP、DB、RPC)都必须显式设 timeout,不能依赖默认值
后台 goroutine 必须统一响应 done channel,不能靠 signal handler 直接 os.Exit()
在 goroutine 里调 os.Exit(0) 会跳过所有 defer,导致 panic: send on closed channel、连接未归还、日志未 flush。
- 正确做法是定义一个全局
done := make(chan struct{}),所有长期 goroutine 都用select { case 响应 - 收到信号后,先
close(done),再调srv.Shutdown(),最后可选time.Sleep()或sync.WaitGroup等 goroutine 自行退出 - 多个信号快速到达时,若未及时消费通道,旧信号可能堆积,导致重复执行 shutdown 逻辑甚至 panic
- 不要在 signal handler goroutine 里启动新 goroutine 去调
os.Exit(),竞态风险高,容易漏执行
最易被忽略的是信号注册时机:必须在 http.Serve、srv.ListenAndServe() 或长期 for 循环之前完成,否则信号可能在注册前就到达,被内核静默丢弃——这个窗口期没有日志、没有提示、无法复现。


















