必须显式调用signal.notify并在主goroutine中启动长期运行逻辑前注册,否则无法捕获sigint/sigterm;需监听syscall.sigint和sigterm,通道缓冲设为1并持续读取,收到信号后应触发优雅关闭而非直接os.exit。

不调 signal.Notify,Go 程序收不到 SIGINT 或 SIGTERM —— 按 Ctrl+C 或 kill -15 会直接退出,清理逻辑一概不执行。
为什么 signal.Notify 必须显式调用且放对位置
Go 不会自动拦截任何信号。signal.Notify 只是把内核发来的信号“转发”到你给的 channel,它本身不阻塞、不清理、不退出。如果写在 http.ListenAndServe 后面,或放进 goroutine 里就让 main 返回,等于没注册。
- 必须在主 goroutine 启动长期运行逻辑(如 HTTP server、for 循环)之前 调用
- 常见错误:把
signal.Notify放进go func() { ... }(),然后 main 立即 return → 整个进程退出,监听 goroutine 根本没机会调度 - 正确顺序:初始化 →
signal.Notify→ 启动 server → 阻塞等待信号(比如或 <code>select{})
监听哪些信号?别只写 os.Interrupt
os.Interrupt 只是 syscall.SIGINT 的别名,线上环境(Kubernetes、systemd、CI 脚本)发的几乎全是 SIGTERM(信号 15)。漏掉它,程序在生产环境就无法优雅退出。
- 底线组合:
syscall.SIGINT,syscall.SIGTERM - 可选加:
syscall.SIGHUP(需配套重载配置逻辑)、syscall.SIGUSR1(如手动触发 dump) - 绝对不要写:
syscall.SIGKILL(信号 9)—— 它根本捕获不到,写了也白写 - 避免
signal.Notify(c)不带参数:它默认监听所有可捕获信号,语义模糊,可能引入SIGUSR2等干扰项
通道必须带缓冲,且要持续读取
用 make(chan os.Signal, 1) 是硬性要求。无缓冲通道在第一次信号到达时就会阻塞;缓冲区大于 1 则容易掩盖设计缺陷(比如 goroutine 挂了没人读,信号越积越多)。
- 错误写法:
go func() { sig := → 只处理第一个信号,后续信号写入满缓冲后被 Go 运行时静默丢弃 - 正确写法:用
for循环持续消费,例如for { sig := - 缓冲设为 1 已足够:信号是事件通知,不是数据流;我们只需要知道“有事发生了”,不需要记录历史
收到信号后不能直接 os.Exit
在 signal handler goroutine 里调 os.Exit 是典型陷阱:它跳过所有 defer、不关闭 listener、不 flush 日志、不等数据库连接归还,还会导致 panic: send on closed channel(因为其他 goroutine 还在往已关 channel 写)。
- 正确做法:让主 goroutine 自己控制退出流程 —— 收到信号后触发
server.Shutdown、db.Close、wg.Wait,最后再os.Exit - HTTP server 关闭必须用
Shutdown(带 context 超时),不是Close;否则活跃请求会被粗暴中断 - 所有长期 goroutine 必须接收
context.Context并自行退出,不能靠 main return “顺便带走”
最常被忽略的一点:信号是否真能抵达进程本身。Docker 默认不转发 SIGTERM,systemd 服务若没配 KillMode=process,会直接杀整个 cgroup。上线前务必用 strace -e trace=rt_sigprocmask,kill 确认信号是否进到了 Go 进程里。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











