根本原因是信号通道未持续读取导致缓冲区满后新信号被内核丢弃;需用 for 循环持续接收,否则仅首次 kill 或 Ctrl+C 有效。

为什么 signal.Notify 只响应一次信号就失效?
根本原因不是 Go 没收到信号,而是你只从信号通道读了一次 —— 通道缓冲区满(make(chan os.Signal, 1))且后续无人消费,新信号被内核静默丢弃。现象是:kill -15 $PID 第一次生效,第二次起无反应;Ctrl+C 同样只触发一次。
- 必须用
for循环持续读取通道,不能只写sig := 一次 - 别把信号处理逻辑写在
if或switch里就return,那 goroutine 直接退出,通道变“死信信箱” - 若需退出整个程序,用
done通道通知主 goroutine,而不是在信号 goroutine 里直接os.Exit(0)(会跳过 defer)
用 syscall.SIGxxx 而不是数字,否则跨平台就崩
syscall.SIGTERM 是常量,值在不同系统上可能不同(比如 Linux 是 15,FreeBSD 是 24),硬写 15 在 macOS 或容器里可能完全不匹配。Windows 更麻烦:SIGUSR1 根本不存在,直接编译报错。
- 始终用
syscall.SIGINT、syscall.SIGTERM等具名常量 - 涉及
SIGUSR1/SIGUSR2时,必须做平台隔离:加// +build linux darwin注释,另写signals_windows.go空实现 -
SIGKILL和SIGSTOP无法捕获或忽略,任何Notify调用对它们都无效
NotifyContext 比手写 goroutine 更安全,尤其要关掉资源时
Go 1.16+ 的 signal.NotifyContext 自动把信号转成 context.Context 的 cancel,天然支持超时、嵌套和资源联动。比自己维护 done 通道少出三类 bug:goroutine 泄漏、信号重复消费、清理逻辑没等完就退出。
- 主逻辑用
select { case 响应退出,业务 goroutine 也能统一监听 - 清理操作务必加超时(比如
time.AfterFunc(5 * time.Second, func() { os.Exit(1) })),防死锁卡住 - 别在
NotifyContext的 ctx 上再套WithTimeout—— 它本身已带 cancel,重复套可能导致提前终止
常见信号该配哪些,别漏掉守护进程必需的 SIGHUP
只监听 SIGINT 和 SIGTERM 够开发调试,但上线后缺 SIGHUP 会导致配置热加载失败,缺 SIGUSR1 会让日志轮转没法做。而多加一个信号几乎零成本。
立即学习“go语言免费学习笔记(深入)”;
- 生产服务建议至少注册:
syscall.SIGINT、syscall.SIGTERM、syscall.SIGHUP -
SIGHUP典型用途:重读配置文件、重建连接池、刷新证书 —— 注意它不等于退出,别在里面调os.Exit - 测试时用
kill -HUP $PID或kill -1 $PID触发,别依赖systemctl reload(它背后也是发信号)
信号处理最易被忽略的点是:你以为程序“收到了”,其实只是内核投递成功,而 Go 运行时是否来得及从通道里拿走它,全看你有没有那个 for 循环兜底。










