Go 中处理软中断的唯一标准路径是 os/signal 包,它将信号转为 channel 消息供 goroutine 消费;必须用带缓冲 channel 监听,常用 SIGINT/SIGTERM,Windows 仅 SIGINT 可靠,需用 select 阻塞接收而非 range,且应分离信号处理与业务逻辑。

Go 中 os/signal 是处理软中断的唯一标准路径
Go 不提供类似 C 的 signal() 或 sigaction() 系统调用封装,也不允许直接注册信号处理器函数。所有软中断(即 Unix 信号)必须通过 os/signal 包统一收口——它本质是把信号转为 Go channel 消息,由你用 Go 语义消费。
这意味着:你不能“拦截并忽略”SIGKILL 或 SIGSTOP,也不能在信号到来时执行任意 C 风格的 handler;你只能监听、接收、然后在自己的 goroutine 里做响应。这是设计使然,不是缺陷。
-
os/signal.Notify必须传入一个带缓冲的 channel(至少make(chan os.Signal, 1)),否则第一次信号就可能丢失 - 监听多个信号时,
syscall.SIGINT和syscall.SIGTERM是最常用组合;SIGHUP在 daemon 场景下用于重载配置 - Windows 下仅
SIGINT可靠生效,其他信号(如SIGTERM)不被系统支持,调用signal.Notify不报错但实际不会触发
select + os.Signal channel 是阻塞等待信号的标准写法
不要用 for range sigs 遍历信号 channel——它会 panic,因为 os.Signal channel 不会关闭。正确方式是用 select 配合阻塞接收,或启动单独 goroutine 处理。
常见错误是把信号接收逻辑和主业务逻辑耦合在同一个 goroutine 里,导致信号来了却卡在耗时操作中无法响应。应始终分离:
- 主 goroutine 负责业务循环(如 HTTP server、worker loop)
- 另起一个 goroutine 专门
<-sigs,收到后发通知(如写入donechannel 或 close 其他资源 channel) - 主 goroutine 用
select同时监听业务事件和退出信号
示例关键片段:
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigChan
log.Println("shutting down...")
// 执行清理
shutdown()
}()
// 主循环
for {
select {
case <-ticker.C:
doWork()
case <-done: // 来自 shutdown() 的完成通知
return
}
}
哪些信号根本不能被 Go 程序捕获或修改行为
SIGKILL(9)和 SIGSTOP(19)是内核强制行为,任何用户态程序都无法注册 handler、无法忽略、无法屏蔽。Go 的 os/signal 对它们完全静默——调用 signal.Notify 传入这两个值不会报错,但永远不会从 channel 收到它们。
另外三类信号需特别注意:
-
SIGSEGV、SIGBUS、SIGFPE:Go 运行时将其转为 panic,不是走os/signal通道。你无法用Notify捕获它们,只能靠recover()(且仅限当前 goroutine) -
SIGPIPE:默认忽略,Go 程序向已关闭 socket 写数据时不会崩溃,但write系统调用返回EPIPE,需检查 error -
SIGPROF:Go 运行时私用,用于 CPU profiling,不应手动监听或干扰
优雅退出时最容易漏掉的 Goroutine 清理点
信号来了只是开始,真正难的是让所有活跃 goroutine 安全退出。常见疏漏包括:
- HTTP server 调用
srv.Shutdown()后没等srv.Close()完成,就直接 return —— 导致连接被粗暴断开 - 后台 worker goroutine 使用
for { ... }死循环,没检查 context.Done() 或退出 channel,变成僵尸 - 使用
time.Ticker时没调用ticker.Stop(),导致 timer leak - 数据库连接池、文件句柄、临时目录等资源未显式释放,依赖 GC 延迟回收
建议统一用 context.WithCancel 控制生命周期,所有长期 goroutine 都监听该 context;信号到来时调用 cancel(),再等待关键 goroutine 退出。
软中断本身很轻量,但让它真正“优雅”,取决于你是否提前想清楚每个并发分支的退出契约。


















