Go程序收不到SIGINT/SIGTERM主因是运行环境未转发信号,需配置Docker--init、systemd KillMode=process等;RabbitMQ消费协程须显式监听多信号、处理完当前消息再Ack/Nack并关闭channel。

Go 程序收不到 SIGINT/SIGTERM?不是代码写错了,大概率是信号根本没进到进程里。 容器、systemd、shell 脚本这些运行环境常会拦截或丢弃信号,signal.Notify 注册再正确也白搭。先确认信号是否抵达,再谈协程怎么退出。
为什么 RabbitMQ 消费协程收不到中断信号
常见现象:按 Ctrl+C 后程序没反应,或者消费协程还在继续拉消息,甚至 panic 报 use of closed network connection。
- 根本原因不是 Go 代码漏了
signal.Notify,而是父进程(如docker run、systemctl start)没把SIGTERM转发给你的主 goroutine - Docker 默认不转发信号,得加
--init参数或用tini;systemd 服务必须配KillMode=process和KillSignal=SIGTERM - RabbitMQ 消费端若用了
autoAck: true,消息一发就确认,程序挂了也查不到未处理消息——这会让“优雅退出”失去意义 - 消费循环里直接
for d := range deliveries是阻塞的,一旦 channel 关闭,循环立即退出,但正在处理的那条消息可能刚解码一半
必须显式监听 SIGINT 和 SIGTERM,SIGHUP 要手动加
Go 的 os/signal 默认只关注 os.Interrupt(即 SIGINT)和 syscall.SIGTERM,SIGHUP 不在默认列表里。如果你要支持终端断开、nohup 启动后重载配置等场景,得自己加上。
-
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)—— 三个信号都注册进去 - 如果程序要作为守护进程长期运行,还得调
syscall.Setpgid(0, 0)成为 session leader,否则 Linux 内核压根不会发SIGHUP给它 - 别用
signal.Notify(sigChan)监听所有信号,某些系统信号(如SIGCHLD)被 Go 运行时内部使用,盲目捕获会导致不可预期行为
消费协程退出前必须完成当前消息 + 手动 Ack/Nack
优雅退出 ≠ 立刻关 channel。RabbitMQ 消费协程的退出点必须卡在“当前消息处理完、且已确认/拒绝”之后,否则消息会卡在 unacked 状态,或被重复投递。
立即学习“go语言免费学习笔记(深入)”;
- 消费循环不能直接
for d := range ch.Consume(...),而要改成for { select { case d, ok := ,这样你才能在收到退出信号后主动跳出循环 - 收到信号后,先设一个
closeFlag或发ctx.Done(),然后等待当前d处理完毕,再调d.Ack(false)或d.Nack(false, true)(重入队) - 切忌在
defer里调Ack:万一业务逻辑 panic,defer仍会执行,导致错误确认 - 消息体
d.Body是复用内存,必须立刻拷贝:data := append([]byte(nil), d.Body),再扔进 goroutine 处理
context.WithCancel + sync.WaitGroup 是协程退出的标配组合
只靠 阻塞主 goroutine 是不够的。HTTP server、DB 连接、后台定时任务、RabbitMQ 消费协程……这些都要同步退出,且必须有超时兜底。
- 启动前创建
ctx, cancel := context.WithCancel(context.Background()),把ctx传给所有长期运行的 goroutine - 每个消费协程开头加
select { case ,避免无意义空转 - 用
sync.WaitGroup记录活跃 consumer 数量,cancel()后调wg.Wait()等它们自然退出 - HTTP server 必须用
srv.Shutdown(ctx)(带超时),不能用srv.Close();RabbitMQ 连接也要在defer里ch.Close()、conn.Close() - 超时时间建议设 5–10 秒,太短可能 kill 掉还没处理完的消息,太长会影响部署节奏
最易被忽略的一点:RabbitMQ 的 ch.Consume() 返回的 deliveries channel 是有缓冲的,哪怕你已经 cancel(),channel 里可能还躺着几条未消费消息。必须在退出前清空它,或确保每条都走到 Ack 分支——否则这些消息会永远卡住,直到连接超时被 RabbitMQ 强制 requeue。


















