必须用带缓冲通道(如make(chan os.signal, 1)),否则信号到达时若无goroutine立即接收,会阻塞或丢弃;无缓冲通道易致信号丢失或主goroutine阻塞,上线后高并发下偶发崩溃。

Go 的 signal 包能可靠监听系统信号,但直接调用 signal.Notify 不等于进程管理——漏掉信号阻塞、goroutine 泄露或未同步退出,进程照样僵死。
为什么 signal.Notify 必须配 os.Signal 通道而不是 chan os.Signal 无缓冲?
无缓冲通道在信号到来时若无 goroutine 即刻接收,会触发 panic(send on closed channel 或阻塞主 goroutine)。实际中必须用带缓冲的通道,容量至少为 1:
sigChan := make(chan os.Signal, 1)
常见错误是写成 make(chan os.Signal),尤其在测试中因信号频率低不易暴露,上线后高并发场景下偶发崩溃。
- 缓冲大小设为 1 足够应对绝大多数情况;设更大(如 10)仅在需容忍多次快速信号(如连续 Ctrl+C)时考虑
- 不要复用同一通道监听多个信号类型,否则无法区分来源;不同信号建议用独立通道或统一用
os.Interrupt, syscall.SIGTERM等显式列表 - Windows 下不支持
SIGUSR1/SIGUSR2,硬编码会导致编译失败,应加// +build !windows构建约束
signal.Stop 和 signal.Reset 什么时候必须调用?
当程序需动态启停信号监听(比如热重载配置时临时屏蔽 SIGHUP),必须调用 signal.Stop 清理注册,否则重复 Notify 会叠加监听,一次信号触发多次处理。
signal.Reset 则用于恢复系统默认行为(比如让 SIGINT 恢复为终止进程),但极少需要——除非你明确接管后又想交还控制权。
- 典型误用:在 HTTP server 启动前调用
signal.Notify,关闭后忘记signal.Stop,下次重启时信号被重复注册 - 更安全的做法是在监听 goroutine 退出时自动清理:
defer signal.Stop(sigChan) - 不要在
main函数末尾调用Reset,此时进程已退出,无效
如何避免 os.Exit 导致资源未释放?
os.Exit 会立即终止进程,跳过 defer、runtime GC、甚至 http.Server.Shutdown。正确做法是用 context 控制生命周期,让各组件主动退出:
ctx, cancel := context.WithCancel(context.Background())
监听到信号后调用 cancel(),再等待关键 goroutine 结束(如 HTTP server 关闭、数据库连接池关闭)。
- HTTP server 示例中,
srv.Shutdown(ctx)必须在cancel()后调用,且需设置超时(如 5s),超时后才调用os.Exit(1) - 数据库连接池(
*sql.DB)需显式调用db.Close(),它不响应 context,必须单独处理 - 不要依赖
runtime.GC()强制回收——无法保证 goroutine 已退出,可能引发 panic
Linux 下 SIGTERM 和 SIGINT 的行为差异会影响哪些场景?
SIGINT(Ctrl+C)默认由终端发送,常用于开发调试;SIGTERM 是 systemd、docker stop、k8s kill 的标准信号,代表“请优雅退出”。两者处理逻辑应一致,但部署环境只发 SIGTERM,若代码只监听 SIGINT,容器就会被强制 KILL(SIGKILL)。
- 生产服务必须同时监听
os.Interrupt(即SIGINT)和syscall.SIGTERM - 某些 init 系统(如 OpenRC)可能发
SIGHUP触发重载,如需支持,得额外加入监听列表 - Docker 默认 10 秒超时后发
SIGKILL,你的 Shutdown 超时必须短于该值,否则不可控终止
真正难的是协调所有子组件的退出顺序和超时边界——信号只是开关,后面一串 cleanup 才决定进程是否真的“优雅”。











