你没法拦截 sigkill 和 sigstop,操作系统禁止用户态程序处理这两个信号;signal.notify 对它们完全静默,进程收到后仍会立刻终止或暂停。

为什么 signal.Notify 不能直接拦截 SIGKILL 和 SIGSTOP
直接回答:你没法拦截 SIGKILL 和 SIGSTOP,操作系统禁止用户态程序处理这两个信号。尝试用 signal.Notify 注册它们不会报错,但进程收到后仍会立刻终止或暂停——signal.Notify 对它们完全静默。真正能细粒度控制的只有其余可捕获信号(如 SIGHUP、SIGINT、SIGTERM、SIGUSR1 等)。
常见错误现象是:在 main 函数里调用 signal.Notify(c, syscall.SIGKILL),然后阻塞等待,结果发现 kill -9 完全没触发 channel 接收。这不是代码写错了,是内核层面限制。
如何用 signal.Notify 实现多信号分类响应
关键不是“拦截”,而是“响应前做清理 + 控制退出节奏”。Golang 的 signal.Notify 本质是把信号转为 channel 消息,你需要自己决定收到后做什么——比如延迟退出、拒绝某些信号、或转发给子进程。
- 用不同 channel 区分信号类型:
sigQuit := make(chan os.Signal, 1)专收SIGQUIT,sigTerm := make(chan os.Signal, 1)专收SIGTERM - 避免用同一个 channel 接收所有信号,否则无法区分
SIGINT(Ctrl+C)和SIGTERM(kill -15)的行为差异 - 注册时显式指定 signal 值,不要依赖
os.Interrupt覆盖全部中断信号;它在 Windows 上映射为os.Kill,行为不一致 - 示例:
signal.Notify(sigQuit, syscall.SIGQUIT) signal.Notify(sigTerm, syscall.SIGTERM, syscall.SIGINT)
怎么让 SIGUSR1 触发配置重载而不中断服务
SIGUSR1 是 Unix 系统中约定俗成的“重新加载配置”信号,Golang 模块里实现它,核心是避免阻塞主 goroutine,且确保重载过程原子、可取消。
- 配置加载逻辑必须是幂等的,因为同一信号可能被多次发送(如运维脚本重复执行
kill -USR1) - 用互斥锁保护配置结构体,但锁范围要小——只锁写,不锁读;读配置时用
atomic.LoadPointer或sync.RWMutex避免影响请求处理 - 不要在 signal handler 里直接调用耗时 IO(如读文件、HTTP 请求),应启动 goroutine 异步处理,并设超时(例如 30 秒),超时则记录 error 并放弃
- 重载失败时,保留旧配置继续运行,而不是 panic 或 exit
子进程信号传递与孤儿进程风险
如果你的模块启动了子进程(比如 exec.Command),默认情况下父进程收到 SIGTERM 后,子进程不会自动收到——它成了孤儿进程,继续跑着没人管。
- 用
syscall.Setpgid(0, 0)在子进程启动后立即创建新进程组,再用syscall.Kill(-pgid, syscall.SIGTERM)向整个组发信号 - 更稳妥的做法是用
os/exec的cmd.Process.Signal()显式传递,但要注意:如果父进程已退出,子进程可能收不到信号,所以建议配合cmd.Wait()或 context 超时管理生命周期 - 监听
SIGHUP时尤其小心:终端断开常触发它,但若子进程依赖 tty,直接转发可能导致子进程异常退出 - 别依赖
os.FindProcess().Signal()查找并杀子进程——PID 可能已被复用,造成误杀
细粒度信号控制最难的部分不在注册和接收,而在状态一致性:信号到达时,你的模块是否处于可安全响应的状态?比如正在写日志文件、持有数据库连接、或正处在 HTTP 处理中间态。这些边界条件不靠 signal 包解决,得靠你自己的状态机和 context 控制。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











