signal.Notify 仅注册信号转发通道,不执行处理;真正处理需通过 goroutine 读取 channel。常见错误是未读 channel 导致程序退出、信号丢失。

signal.Notify 是注册动作,不是信号处理器本身
很多人误以为调用 signal.Notify 就等于“装好了信号处理函数”,其实它只做一件事:告诉 Go 运行时「把指定信号转发到这个 channel」。信号本身仍由操作系统发出,Go 只是搭了一条转发通道——真正消费信号的,是你后续对 channel 的读取逻辑。
常见错误包括:
- 只调用
signal.Notify,但没开 goroutine 读 channel,主 goroutine 一结束,程序就退出,信号根本来不及送达 - 用
sig := 读一次就完事,第二次发 SIGTERM 就丢信号(缓冲区满且无后续读取) - 传了空参数
signal.Notify(c),结果连SIGCHLD都被转发进来,干扰主逻辑
channel 缓冲大小设为 1 是合理底线,但必须配 for 循环
make(chan os.Signal, 1) 足够应对绝大多数场景:信号是离散事件,不是流数据;设更大(比如 10)反而会掩盖设计缺陷——比如本该阻塞等待清理完成,却因缓冲“吞掉”后续信号而误判状态。
关键在消费端:
立即学习“go语言免费学习笔记(深入)”;
- 错误写法:
go func() { sig := —— 只处理第一个 - 正确写法:
go func() { for sig := range sigCh { handle(sig) } }() - 如果处理逻辑可能阻塞(如 HTTP Server.Shutdown),建议把耗时操作扔进新 goroutine,避免卡住信号循环
os.Interrupt 和 syscall.SIGINT 不等价,跨平台要小心
os.Interrupt 在 Unix 系统上等于 syscall.SIGINT,但在 Windows 上是 os.Kill(即强制终止),无法被捕获。生产环境若需统一行为,应显式使用 syscall.SIGINT 和 syscall.SIGTERM,而不是依赖 os.Interrupt。
更现实的问题是:Kubernetes、systemd、docker stop 默认发的是 SIGTERM,不是 SIGINT。漏监听 syscall.SIGTERM 是线上服务无法优雅退出的高频原因。
- 推荐写法:
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) - 避免写法:
signal.Notify(sigCh, os.Interrupt)(Windows 下失效,Linux 下漏 SIGTERM) - 不要写
signal.Notify(sigCh)(默认监听所有可捕获信号,含SIGUSR1等,语义模糊易出错)
子进程信号继承靠默认行为,不是靠“手动转发”
Go 程序里启动子进程(exec.Command)后,不需要写代码“把收到的 SIGTERM 转发给子进程”。只要父进程没调用 signal.Ignore 或 syscall.Syscall 修改 signal mask,子进程会自动继承父进程对信号的处置方式(默认是终止)。
真正要注意的是:
- 父进程收到信号后,别急着
os.Exit(0),得先等子进程退出(比如用cmd.Wait()) - 如果子进程是守护进程或后台服务,可能需要显式设置
cmd.SysProcAttr.Setpgid = true避免信号被 shell 拦截 - 用
signal.Stop(sigCh)清理监听器,通常在 shutdown 流程末尾调用,防止残留 goroutine
signal.Notify 注册的 channel → 你的 for range 循环 → 处理逻辑。容易出问题的环节不在“怎么转发”,而在“谁来收”和“收完干啥”。缓冲、循环、信号列表这三点没对齐,信号就悄无声息地丢了。


















