signal.Notify必须配合带缓冲channel(如make(chan os.Signal, 1)),否则未读信号会被内核静默丢弃;主goroutine需持续运行至清理完成,不可直接return;HTTP优雅退出必须用srv.Shutdown(ctx)并配合context超时与req.Context().Done()检查。

signal.Notify 必须配带缓冲 channel
直接写 make(chan os.Signal) 会丢信号——无缓冲 channel 在未被读取前,新信号到来就阻塞发送方(即内核),而 Go 运行时对信号的投递不保证重试,连续按 Ctrl+C 或 K8s 发 SIGTERM 时极易漏收。
- 永远用
make(chan os.Signal, 1),哪怕只监听一个信号 - 若需同时响应
syscall.SIGTERM和syscall.SIGINT,仍只需一个 channel,signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT)即可 - 不要用
chan int或chan string接信号,必须是chan os.Signal,否则signal.Notify静默失败
主 goroutine 不能退出,否则清理逻辑白写
常见错误是收到信号后直接调用 cleanup() 然后 return,main 函数一结束,整个进程立刻终止,正在执行的 goroutine(比如数据库连接关闭、HTTP 响应刷盘)全被粗暴打断。
- 必须让 main goroutine 持续存活,直到所有清理完成——最简方式是
select {}阻塞,或用sync.WaitGroup等待 shutdown goroutine 结束 - 清理动作要另起 goroutine 执行:
go func() { cleanup(); os.Exit(0) }(),避免阻塞信号接收 -
defer cleanup()完全无效:SIGTERM 不触发函数返回,defer根本不会跑
http.Server.Shutdown 是唯一靠谱的 HTTP 优雅退出路径
别碰 server.Close(),它会立刻关 listener 并 reset 所有活跃连接,客户端看到的就是 connection reset by peer,上传中断、WebSocket 断连全算你头上。
- 必须用
srv.Shutdown(ctx),且ctx要带超时:ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second) - 超时值建议比 K8s 的
terminationGracePeriodSeconds小 5 秒,留出系统级清理余量 - 务必忽略
http.ErrServerClosed:这是ListenAndServe()正常退出标志,不是错误 - handler 内部必须检查
req.Context().Done(),否则Shutdown()会无限等待那些没响应 cancel 的请求
goroutine 泄漏和 context 超时怎么配合优雅退出
仅停掉 listener 不够,已启动的后台任务(定时器、消息消费、长轮询 handler)还在跑,进程就卡死不退出。
- 所有长期 goroutine 启动时必须接收同一个
context.Context,并在select中监听ctx.Done() - 用
context.WithCancel而非WithTimeout控制生命周期更稳妥:信号一来就cancel(),各组件自行决定退出节奏 - DB 查询必须用
db.QueryContext(ctx, ...),HTTP 客户端必须传ctx,否则这些底层阻塞点会拖垮整个 shutdown 流程


















