必须显式调用 signal.notify 才能捕获 sigint/sigterm,否则程序直接退出;需在主 goroutine 阻塞前注册,且不能置于 http.listenandserve 等阻塞调用之后,否则监听失效。

不调用 signal.Notify,Go 程序根本收不到 SIGINT 或 SIGTERM——按 Ctrl+C、执行 kill -15 $PID 都会直接退出,清理逻辑一概不执行。
signal.Notify 必须显式调用且放对位置
Go 运行时不会自动拦截任何信号。signal.Notify 只是把内核发来的信号“转发”到你给的 channel,它本身不阻塞、不清理、不退出。常见错误包括:
- 把
signal.Notify写在http.ListenAndServe后面,主 goroutine 已返回,监听失效 - 放进
go func() { }()里就让main返回 → 整个进程立刻退出,goroutine 根本没机会调度 - 在启动长期循环(如
for { select { ... } })之前没注册,信号可能已在监听 setup 完成前抵达,被内核静默丢弃
正确顺序必须是:初始化 → signal.Notify → 启动 server / loop → 阻塞等待信号(比如 )。
必须同时监听 syscall.SIGINT 和 syscall.SIGTERM
只监听 os.Interrupt(即 SIGINT)是开发环境惯性,线上几乎无效。Kubernetes、systemd、CI 脚本、Docker stop 默认发的都是 SIGTERM(信号 15)。漏掉它,程序在生产环境就卡住不退出。
- 底线组合:
syscall.SIGINT, syscall.SIGTERM - 可选加:
syscall.SIGHUP(需配套重载配置逻辑)、syscall.SIGUSR1(如手动触发 profile dump) - 绝对不要写:
syscall.SIGKILL—— 它不可捕获,写了也白写 - 避免
signal.Notify(c)不带参数:默认监听所有可捕获信号,语义模糊,可能引入SIGUSR2等干扰项
channel 必须带缓冲且持续读取
用 make(chan os.Signal, 1) 是底线,但更关键的是消费方式。信号是异步事件,不是流数据;设更大缓冲反而掩盖设计缺陷。
- 错误写法:
sig := 只读一次 → 第二次信号到来时缓冲区满,Go 运行时直接丢弃,毫无提示 - 正确写法:用
for循环持续消费,例如for range sigChan { ... }或for { sig := - 缓冲大小设为 1 足够:它只是防“信号到达瞬间未开始监听”的竞态,不是为了攒多个信号
信号收到后不能直接 os.Exit,要触发优雅关闭
优雅退出 ≠ 立即退出。http.Server.Shutdown 不是关开关,而是发通知 + 等收尾。若依赖组件没配合 context,就会超时失败,最终强制关闭连接。
- 启动前创建
ctx, cancel := context.WithCancel(context.Background()),把ctx传给所有需要响应退出的组件 - 每个长期 goroutine 开头加
select { case ,数据库查询用 <code>db.QueryContext(ctx, ...),HTTP client 用http.NewRequestWithContext(ctx, ...) - HTTP server 关闭必须带超时:
srv.Shutdown(ctx),而不是srv.Close() - 用
sync.WaitGroup记录活跃 worker,cancel()后调wg.Wait()等它们结束
最容易被忽略的是:信号是否真传到了进程里。Docker 默认不转发 SIGTERM,systemd 若没配 KillMode=process,会直接杀整个 cgroup。得用 strace -e trace=rt_sigprocmask,rt_sigaction,kill 确认信号是否抵达,而不是只查 Go 代码有没有写对。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











