Golang信号处理实战
作者:言之。
本文介绍了Go语言中os/signal包的基本用法,用于处理Unix系统中的异步信号,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
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 main
import (
"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 main
import (
"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("业务清理完成")
return
default:
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 main
import (
"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
- 容器化部署时能优雅退出
- 确保连接处理完成后再关闭
到此这篇关于Golang信号处理实战的文章就介绍到这了,更多相关Golang信号处理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
