Golang信号处理实战
Go os/signal
入门与实战
1. 为什么需要信号处理
在类 Unix 系统中,信号(Signal)是一种异步通知机制,内核通过它告诉进程发生了某种事件,比如:
- 终止进程:
SIGTERM
(kill 发送的默认信号)、SIGINT
(Ctrl+C) - 挂起/恢复:
SIGTSTP
(Ctrl+Z) - 重新加载配置:
SIGHUP
- 自定义信号:
SIGUSR1
、SIGUSR2
如果不处理,进程会使用 默认行为(可能直接退出)。
而 os/signal
包让我们在用户态捕获这些信号,并执行自定义逻辑(比如优雅退出、保存状态、重载配置等)。
2. 核心 API
函数 | 功能 | 常见用途 |
---|---|---|
Notify(c chan<- os.Signal, sig ...os.Signal) | 将指定信号转发到 c | 订阅信号 |
Stop(c chan<- os.Signal) | 停止向 c 转发信号 | 取消订阅 |
Ignore(sig ...os.Signal) | 忽略信号(不再转发给程序) | 屏蔽特定信号 |
Reset(sig ...os.Signal) | 恢复信号默认行为 | 信号处理恢复默认 |
NotifyContext(ctx, sig...) | 返回会在收到信号时自动 cancel 的 Context | 优雅退出 |
3. 基本使用
示例:监听 SIGINT
(Ctrl+C)和 SIGTERM
(kill)
package mainimport ("fmt""os""os/signal""syscall"
)func main() {sigChan := make(chan os.Signal, 1)// 订阅两个信号signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)fmt.Println("程序启动,等待信号...")sig := <-sigChan // 阻塞等待fmt.Println("收到信号:", sig)fmt.Println("执行清理逻辑...")// 这里做关闭文件、断开连接等操作fmt.Println("程序退出")
}
运行:
go run main.go
# Ctrl+C 或 kill PID 会触发信号
4. 使用 NotifyContext
优雅退出
Go 1.16+ 引入的 NotifyContext
结合 context
让信号处理更简洁。
package mainimport ("context""fmt""os/signal""syscall""time"
)func main() {ctx, stop := signal.NotifyContext(context.Background(),syscall.SIGINT, syscall.SIGTERM)defer stop()fmt.Println("程序启动,等待信号...")// 模拟业务协程go func() {for {select {case <-ctx.Done():fmt.Println("业务收到退出信号,清理中...")time.Sleep(1 * time.Second)fmt.Println("业务清理完成")returndefault:fmt.Println("业务运行中...")time.Sleep(2 * time.Second)}}}()<-ctx.Done() // 阻塞,直到信号触发fmt.Println("主程序退出")
}
好处:
- 自动取消 context
- 不用自己建 channel
- 多个 goroutine 可同时感知退出
5. 高级用法
5.1 忽略信号
signal.Ignore(syscall.SIGPIPE) // 忽略管道断开
5.2 动态取消订阅
signal.Stop(sigChan) // 取消 channel 的订阅
5.3 同时监听多个信号
signal.Notify(sigChan) // 不指定信号时,监听所有信号
不推荐监听全部信号,可能会拦截 SIGKILL、SIGSTOP 等无法处理的信号。
6. 原理机制
简化版流程:
Notify
注册信号 → 调用 runtime 的enableSignal(n)
。- runtime 捕获信号后调用
process()
。 process
遍历所有 channel handler,非阻塞发送信号。Stop
时调用disableSignal(n)
,等待 runtime 信号队列清空(signalWaitUntilIdle()
)。
特点:
- 非阻塞投递:channel 必须有缓冲,否则可能丢信号。
- 引用计数:多个 channel 可监听同一信号,ref=0 时才会真正停止捕获。
- bitmask 存储:handler 用 bit 位记录关注的信号,内存占用小。
7. 最佳实践
-
总是用缓冲 channel
make(chan os.Signal, 1)
避免信号丢失。
-
优雅退出而不是强杀
在SIGTERM
里做清理,配合context
实现安全收尾。 -
避免监听全部信号
只订阅需要的信号,避免影响系统默认行为。 -
多 goroutine 协同
用NotifyContext
让所有协程通过<-ctx.Done()
感知退出。 -
容器化部署必备
Docker 默认用SIGTERM
停止容器,业务代码应处理此信号。
8. 实战案例:优雅关闭 HTTP 服务器
package mainimport ("context""fmt""net/http""os/signal""syscall""time"
)func main() {srv := &http.Server{Addr: ":8080"}// 启动 HTTP 服务go func() {fmt.Println("HTTP 服务启动在 :8080")if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {fmt.Println("HTTP 服务器出错:", err)}}()// 信号监听ctx, stop := signal.NotifyContext(context.Background(),syscall.SIGINT, syscall.SIGTERM)defer stop()<-ctx.Done() // 等待信号fmt.Println("收到退出信号,正在关闭服务器...")// 设置超时的优雅关闭shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)defer cancel()if err := srv.Shutdown(shutdownCtx); err != nil {fmt.Println("服务器关闭错误:", err)}fmt.Println("服务器已优雅退出")
}
这样写的好处:
- 支持 Ctrl+C / kill
- 容器化部署时能优雅退出
- 确保连接处理完成后再关闭