signal.notify 必须在主 goroutine 阻塞前调用,否则信号无法监听;正确顺序是初始化→signal.notify→启动长期运行逻辑→阻塞等待信号;通道至少需缓冲1,且必须及时消费,否则信号丢失。

signal.Notify 必须在主 goroutine 阻塞前调用
Go 不会自动拦截任何信号,signal.Notify 只是注册转发规则,不执行阻塞、不触发退出。如果写在 http.ListenAndServe 或 srv.Serve 之后,主 goroutine 已进入系统调用阻塞,后续信号根本无法被监听到——进程会被内核直接终止,清理逻辑全失效。
常见错误模式:
-
go func() { signal.Notify(...) }()→ 主 goroutine 立即 return,整个进程退出 -
http.ListenAndServe(...); signal.Notify(...)→ 信号注册永远没机会生效
正确顺序必须是:初始化 → signal.Notify → 启动长期运行逻辑(如 HTTP server)→ 阻塞等待信号。哪怕只是 for {},也得确保主 goroutine 持续存活。
通道缓冲和消费方式决定信号是否丢失
make(chan os.Signal, 1) 是底线配置。无缓冲通道在第一个信号到达时就阻塞,导致后续信号被 Go 运行时静默丢弃;缓冲区大于 1 容易掩盖设计缺陷(比如 goroutine 挂了没人读,信号越积越多)。
更关键的是消费方式:
- 错误写法:
sig := 只读一次 → 第二个 <code>kill -15就失效 - 正确写法:用
for { sig := 持续消费,确保每次信号都能进处理逻辑
信号是事件通知,不是数据流。设缓冲为 1 + 循环读取,才是稳定可靠的做法。
Go语言(Golang)1.26.0版本提供 Go 官方 Windows amd64 MSI 安装包下载入口,版本号 1.26.0,可用于旧项目维护、兼容性测试和指定版本开发环境配置。
必须同时监听 syscall.SIGINT 和 syscall.SIGTERM
os.Interrupt 只是 syscall.SIGINT 的别名,对应 Ctrl+C;但生产环境(Kubernetes、systemd、CI 脚本)发的几乎全是 syscall.SIGTERM(信号 15)。只监听前者,服务在容器里一杀就丢数据。
别写 signal.Notify(c) 不带参数——它默认监听所有可捕获信号,包括 SIGUSR2 等干扰项,语义模糊且易引发意外行为。
推荐组合:signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM),显式、安全、覆盖主要退出场景。
收到信号后不能直接 os.Exit
在 signal handler goroutine 里调 os.Exit 是硬伤:跳过所有 defer、不关闭 listener、不 flush 日志、不等数据库连接归还,还会引发 panic: send on closed channel。
正确路径是让主 goroutine 控制退出节奏:
- 收到信号后,先触发
srv.Shutdown(ctx)(ctx必须带超时,如 10s) - 再 close 全局
donechannel 或 cancelcontext.WithCancel - 用
sync.WaitGroup等待所有 worker goroutine 退出 - 最后才真正退出 main
最容易被忽略的不是“怎么写”,而是“信号注册时机”和“通道消费是否持续”——它们不报错,但会让程序在线上「看起来能停,实际一杀就丢数据」。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










