Go命令行程序必须在main goroutine中用signal.Notify注册信号监听,否则进程退出导致监听失效;信号处理应仅设标志或发通知,清理逻辑放main中执行并加超时,禁止在handler中做I/O、锁等阻塞操作。

Go 命令行程序不处理信号(如 SIGINT、SIGTERM)时,Ctrl+C 会直接终止进程,无法执行清理逻辑——这不是“没响应”,而是根本没注册监听器。
为什么 signal.Notify 必须在 main goroutine 里调用
Go 的 signal.Notify 要求接收 channel 必须是未关闭的、且由调用方负责生命周期。如果在子 goroutine 中注册,main 退出后整个程序立即终止,信号监听器形同虚设。
- 必须在
main()开头就创建signals := make(chan os.Signal, 1),缓冲大小为 1 防止信号丢失 -
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)后,不能让signals被 GC 或提前关闭 - 监听逻辑必须用
go func() { for range signals { ... } }()启动,否则会阻塞 main - 别用
os.Interrupt替代syscall.SIGINT:Windows 下行为不一致,跨平台应统一用syscall常量
收到信号后不能直接 os.Exit(0)
直接退出跳过了所有 defer、资源释放和状态持久化,数据库连接不关闭、临时文件不清理、日志缓冲不 flush——这在 CLI 工具中等于制造数据残缺。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 正确做法是用一个
done chan struct{}通知主流程开始收尾 - 在信号 handler 里只做两件事:打印提示 + 写入
done,其余清理逻辑放在 main 中统一执行 - 务必给关键清理操作加超时,比如
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second),防止db.Close()卡死 - 如果程序依赖子进程(如 exec.Command),需显式调用
cmd.Process.Kill(),否则子进程会变成孤儿
如何避免信号处理期间 panic 或死锁
信号 handler 是同步回调,不是独立 goroutine;在其中调用阻塞函数(如 log.Printf、fmt.Println)或锁操作,极易触发死锁或 runtime panic。
立即学习“go语言免费学习笔记(深入)”;
- handler 内禁止任何 I/O、网络、锁、channel 操作(除了向已知安全的 channel 发送)
- 不要在 handler 里调用
time.Sleep或等待其他 goroutine —— 它会阻塞整个信号调度循环 - 若需记录日志,改用
log.SetOutput(os.Stderr)后直接写os.Stderr.Write(),绕过 log 包的锁 - 最简健壮模式:
atomic.StoreInt32(&shutdownFlag, 1)+ 主循环定期检查该标志,把复杂逻辑完全移出 handler
真正难的不是注册信号,而是定义“清理完成”的边界:数据库事务是否要回滚?正在写的文件要不要补全 header?这些没有标准答案,只能按业务语义硬编码判断。信号只是中断开关,怎么关,得你自己画线。

















