Go os/signal 包详解
概述
os/signal 包实现了对传入信号的访问。信号主要在类 Unix 系统上使用。
重要说明:
- ✓ 处理传入的系统信号
- ✓ 主要用于 Unix-like 系统
- ✓ Go 1.0+ 引入
- ✓ 支持异步信号通知
- ✓ 支持上下文集成(Go 1.16+)
- ✓ Windows 和 Plan 9 支持有限
信号类型:
- 同步信号:SIGBUS、SIGFPE、SIGSEGV(由程序执行错误触发)
- 异步信号:SIGHUP、SIGINT、SIGQUIT 等(由内核或其他程序发送)
不能捕获的信号:
- SIGKILL - 不能被捕获或忽略
- SIGSTOP - 不能被捕获或忽略
包导入
import (
"os/signal"
)
基本使用
1. 捕获中断信号
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
)
func main() {
// 创建信号通道
sigChan := make(chan os.Signal, 1)
// 订阅 SIGINT 和 SIGTERM
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
fmt.Println("等待信号...(按 Ctrl+C 测试)")
// 等待信号
sig := <-sigChan
fmt.Printf("收到信号:%v\n", sig)
// 清理
signal.Stop(sigChan)
}
2. 优雅关闭
package main
import (
"fmt"
"os"
"os/signal"
"time"
)
func main() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt)
go func() {
sig := <-sigChan
fmt.Printf("\n收到信号:%v,开始清理...\n", sig)
// 执行清理操作
time.Sleep(1 * time.Second)
fmt.Println("清理完成,退出")
os.Exit(0)
}()
// 主程序工作
for {
fmt.Println("工作中...")
time.Sleep(1 * time.Second)
}
}
一、变量
本包没有导出变量。
二、类型
本包没有导出类型。
三、函数(按 a-z 排序)
Ignore
func Ignore(sig ...os.Signal)
Ignore 使提供的信号被忽略。如果程序收到这些信号,不会发生任何事情。Ignore 撤销任何先前对提供信号的 Notify 调用的效果。
参数:
sig- 要忽略的信号列表
说明:
- 如果没有提供信号,所有传入信号都将被忽略
- 撤销 Notify 的效果
示例:
// 忽略 SIGINT(Ctrl+C)
signal.Ignore(syscall.SIGINT)
// 忽略多个信号
signal.Ignore(syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
// 忽略所有信号(不推荐)
signal.Ignore()
警告:
// ✗ 危险 - 忽略所有信号
signal.Ignore()
// 程序将无法被中断或终止
Ignored
func Ignored(sig os.Signal) bool
Ignored 报告 sig 当前是否被忽略。
参数:
sig- 要检查的信号
返回值:
bool- 如果信号被忽略则返回 true
示例:
if signal.Ignored(syscall.SIGINT) {
fmt.Println("SIGINT 当前被忽略")
} else {
fmt.Println("SIGINT 未被忽略")
}
Notify
func Notify(c chan<- os.Signal, sig ...os.Signal)
Notify 使包 signal 将传入的信号中继到 c。
参数:
c- 接收信号的通道sig- 要订阅的信号列表(可选)
说明:
- 如果没有提供信号,所有传入信号都将中继到 c
- 包 signal 不会阻塞发送到 c:调用者必须确保 c 有足够的缓冲空间
- 对于仅用于通知单个信号值的通道,大小为 1 的缓冲区就足够了
- 允许使用同一通道多次调用 Notify:每次调用都会扩展到发送到该通道的信号集
- 从集合中移除信号的唯一方法是调用 Stop
- 允许使用不同通道和相同信号多次调用 Notify:每个通道独立接收传入信号的副本
示例:
// 创建缓冲通道
sigChan := make(chan os.Signal, 1)
// 订阅特定信号
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
// 订阅所有信号
signal.Notify(sigChan)
// 等待信号
sig := <-sigChan
fmt.Printf("收到信号:%v\n", sig)
重要:
// ✓ 正确 - 使用缓冲通道
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt)
// ✗ 错误 - 无缓冲通道可能阻塞
sigChan := make(chan os.Signal) // 无缓冲
signal.Notify(sigChan, os.Interrupt)
// 如果没有及时读取,信号可能丢失
NotifyContext
func NotifyContext(parent context.Context, signals ...os.Signal) (ctx context.Context, stop context.CancelFunc)
NotifyContext 返回父上下文的副本,当列出的信号之一到达、调用返回的 stop 函数或父上下文的 Done 通道关闭时,该副本标记为完成(其 Done 通道关闭),以先发生者为准。
参数:
parent- 父上下文signals- 要监听的信号列表
返回值:
ctx- 新的上下文stop- 停止函数(取消信号监听)
说明:
- stop 函数取消信号行为,类似于 signal.Reset
- 调用 stop 会释放与之关联的资源
- 代码应在该上下文中运行的操作完成后尽快调用 stop
示例:
// 创建带信号监听的上下文
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
defer stop()
// 等待上下文取消
<-ctx.Done()
fmt.Println("收到信号,上下文取消")
完整示例:
package main
import (
"context"
"fmt"
"os"
"os/signal"
"time"
)
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
// 启动后台工作
go func() {
for {
select {
case <-ctx.Done():
fmt.Println("工作取消")
return
default:
fmt.Println("工作中...")
time.Sleep(1 * time.Second)
}
}
}()
// 等待上下文完成
<-ctx.Done()
fmt.Println("程序退出")
}
Reset
func Reset(sig ...os.Signal)
Reset 撤销任何先前对提供信号的 Notify 调用的效果。
参数:
sig- 要重置的信号列表
说明:
- 如果没有提供信号,所有信号处理程序都将被重置
- 恢复系统默认行为
示例:
// 订阅信号
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt)
// ... 使用一段时间后 ...
// 重置,恢复默认行为
signal.Reset(os.Interrupt)
// 现在按 Ctrl+C 会直接退出程序
Stop
func Stop(c chan<- os.Signal)
Stop 使包 signal 停止将传入的信号中继到 c。它撤销所有先前使用 c 调用 Notify 的效果。
参数:
c- 要停止的信号通道
说明:
- 当 Stop 返回时,保证 c 不会再收到任何信号
示例:
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt)
// ... 使用一段时间后 ...
// 停止监听
signal.Stop(sigChan)
// 现在 sigChan 不会再收到信号
四、典型示例
示例 1:基本的信号捕获
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("等待信号...(按 Ctrl+C)")
sig := <-sigChan
fmt.Printf("收到信号:%v\n", sig)
signal.Stop(sigChan)
}
示例 2:优雅关闭服务器
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
server := &http.Server{Addr: ":8080"}
go func() {
fmt.Println("服务器启动在 :8080")
if err := server.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}()
// 等待中断信号
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
fmt.Println("\n收到关闭信号,开始优雅关闭...")
// 创建带超时的上下文
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// 优雅关闭服务器
if err := server.Shutdown(ctx); err != nil {
log.Printf("关闭错误:%v\n", err)
}
fmt.Println("服务器已关闭")
}
示例 3:处理多个信号
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
)
func main() {
sigChan := make(chan os.Signal, 1)
// 订阅多个信号
signal.Notify(sigChan,
syscall.SIGINT, // Ctrl+C
syscall.SIGTERM, // 终止信号
syscall.SIGHUP, // 挂起信号
syscall.SIGQUIT, // 退出信号
)
fmt.Println("等待信号...")
for sig := range sigChan {
fmt.Printf("收到信号:%v\n", sig)
switch sig {
case syscall.SIGINT:
fmt.Println(" - 这是 Ctrl+C 中断")
case syscall.SIGTERM:
fmt.Println(" - 这是终止信号")
signal.Stop(sigChan)
return
case syscall.SIGHUP:
fmt.Println(" - 这是挂起信号,可以重新加载配置")
case syscall.SIGQUIT:
fmt.Println(" - 这是退出信号")
}
}
}
示例 4:使用 NotifyContext
package main
import (
"context"
"fmt"
"os"
"os/signal"
"time"
)
func worker(ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d 停止\n", id)
return
default:
fmt.Printf("Worker %d 工作中...\n", id)
time.Sleep(1 * time.Second)
}
}
}
func main() {
// 创建带信号监听的上下文
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
// 启动多个 worker
for i := 0; i < 3; i++ {
go worker(ctx, i)
}
// 等待信号
<-ctx.Done()
fmt.Println("收到中断信号,所有 worker 停止")
// 等待 worker 完成
time.Sleep(2 * time.Second)
}
示例 5:忽略特定信号
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
// 忽略 SIGHUP
signal.Ignore(syscall.SIGHUP)
// 订阅其他信号
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
fmt.Println("SIGHUP 被忽略,按 Ctrl+C 退出")
// 定期发送 SIGHUP 给自己测试
go func() {
for {
time.Sleep(2 * time.Second)
syscall.Kill(os.Getpid(), syscall.SIGHUP)
}
}()
<-sigChan
fmt.Println("退出")
}
示例 6:检查信号是否被忽略
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
)
func main() {
fmt.Printf("SIGINT 被忽略?%v\n", signal.Ignored(syscall.SIGINT))
// 忽略 SIGINT
signal.Ignore(syscall.SIGINT)
fmt.Printf("忽略后,SIGINT 被忽略?%v\n", signal.Ignored(syscall.SIGINT))
// 恢复
signal.Reset(syscall.SIGINT)
fmt.Printf("重置后,SIGINT 被忽略?%v\n", signal.Ignored(syscall.SIGINT))
}
示例 7:信号去重
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGUSR1)
go func() {
for sig := range sigChan {
fmt.Printf("收到信号:%v\n", sig)
// 处理信号...
}
}()
// 模拟快速发送多个信号
time.AfterFunc(1*time.Second, func() {
for i := 0; i < 5; i++ {
syscall.Kill(os.Getpid(), syscall.SIGUSR1)
}
})
// 等待处理
time.Sleep(3 * time.Second)
}
示例 8:条件信号处理
package main
import (
"fmt"
"os"
"os/signal"
"sync/atomic"
"syscall"
"time"
)
func main() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGUSR1)
var enabled int32 = 1
go func() {
for sig := range sigChan {
if atomic.LoadInt32(&enabled) == 1 {
fmt.Printf("处理信号:%v\n", sig)
} else {
fmt.Printf("信号处理已禁用,忽略:%v\n", sig)
}
}
}()
// 启用/禁用处理
time.AfterFunc(2*time.Second, func() {
fmt.Println("禁用信号处理")
atomic.StoreInt32(&enabled, 0)
})
time.AfterFunc(4*time.Second, func() {
fmt.Println("启用信号处理")
atomic.StoreInt32(&enabled, 1)
})
// 发送信号
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for i := 0; i < 6; i++ {
<-ticker.C
syscall.Kill(os.Getpid(), syscall.SIGUSR1)
}
time.Sleep(1 * time.Second)
}
五、最佳实践
1. 总是使用缓冲通道
// ✓ 正确 - 使用缓冲通道
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt)
// ✗ 错误 - 无缓冲通道
sigChan := make(chan os.Signal) // 可能丢失信号
2. 总是清理资源
// ✓ 正确 - 使用 defer 清理
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt)
defer signal.Stop(sigChan)
// 或使用 NotifyContext
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
3. 优雅关闭
// ✓ 推荐 - 优雅关闭模式
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigChan
// 执行清理
cleanup()
os.Exit(0)
}()
// 主程序工作
4. 使用 NotifyContext 简化代码
// ✓ 现代方式 - 使用 NotifyContext
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
select {
case <-ctx.Done():
fmt.Println("收到信号")
case <-time.After(10 * time.Second):
fmt.Println("超时")
}
// ✗ 传统方式 - 需要手动管理
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt)
// 需要手动调用 signal.Stop
5. 正确处理多个信号
// ✓ 正确 - 明确订阅需要的信号
signal.Notify(sigChan,
syscall.SIGINT,
syscall.SIGTERM,
syscall.SIGHUP,
)
// ✗ 不推荐 - 订阅所有信号
signal.Notify(sigChan) // 可能收到意外信号
6. 信号处理不阻塞
// ✓ 正确 - 在 goroutine 中处理
go func() {
for sig := range sigChan {
go handleSignal(sig) // 异步处理
}
}()
// ✗ 错误 - 阻塞主流程
for sig := range sigChan {
handleSignal(sig) // 可能阻塞
}
六、与其他包配合
1. 与 context 包配合
import (
"context"
"os/signal"
)
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
// 使用 ctx 控制 goroutine
2. 与 os 包配合
import (
"os"
"os/signal"
"syscall"
)
// 使用 os.Signal 接口
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt)
// 或使用 syscall 包
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
3. 与 time 包配合
import (
"os/signal"
"time"
)
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt)
select {
case sig := <-sigChan:
fmt.Println("收到信号:", sig)
case <-time.After(10 * time.Second):
fmt.Println("超时")
}
4. 与 net/http 包配合
import (
"net/http"
"os/signal"
"syscall"
)
server := &http.Server{Addr: ":8080"}
go func() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
server.Shutdown(ctx)
}()
server.ListenAndServe()
七、快速参考
函数总览
| 函数 | 说明 |
|---|---|
| Ignore | 忽略信号 |
| Ignored | 检查信号是否被忽略 |
| Notify | 订阅信号 |
| NotifyContext | 创建带信号监听的上下文 |
| Reset | 重置信号处理 |
| Stop | 停止信号订阅 |
常用信号
| 信号 | 说明 | 触发方式 |
|---|---|---|
| SIGINT | 中断信号 | Ctrl+C |
| SIGTERM | 终止信号 | kill 命令 |
| SIGHUP | 挂起信号 | 终端断开 |
| SIGQUIT | 退出信号 | Ctrl+\ |
| SIGUSR1 | 用户定义信号 1 | 自定义 |
| SIGUSR2 | 用户定义信号 2 | 自定义 |
| SIGCHLD | 子进程状态改变 | 子进程退出 |
| SIGPIPE | 管道破裂 | 写入关闭的管道 |
信号处理模式
| 模式 | 函数 |
|---|---|
| 捕获信号 | Notify |
| 忽略信号 | Ignore |
| 恢复默认 | Reset |
| 停止监听 | Stop |
| 上下文集成 | NotifyContext |
| 检查状态 | Ignored |
平台差异
| 系统 | 支持 |
|---|---|
| Unix/Linux | 完整支持 |
| Windows | 有限支持(SIGINT、SIGTERM) |
| Plan 9 | 使用 Note 类型 |
八、注意事项
1. 不能捕获的信号
// SIGKILL 和 SIGSTOP 不能被捕获
signal.Notify(sigChan, syscall.SIGKILL) // 无效
signal.Notify(sigChan, syscall.SIGSTOP) // 无效
2. 同步信号转为 panic
// 同步信号(SIGBUS、SIGFPE、SIGSEGV)会转为 panic
// 不应使用 signal.Notify 捕获
3. 通道必须有缓冲
// ✓ 正确
sigChan := make(chan os.Signal, 1)
// ✗ 错误
sigChan := make(chan os.Signal) // 无缓冲,可能丢失信号
4. 默认行为
// 默认行为:
// - SIGINT、SIGTERM -> 程序退出
// - SIGQUIT、SIGILL、SIGTRAP 等 -> 退出并堆栈转储
// - SIGTSTP、SIGTTIN、SIGTTOU -> 系统默认(作业控制)
// - 其他信号 -> 捕获但不采取行动
5. NotifyContext 资源清理
// ✓ 正确 - 使用 defer 清理
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
// ✗ 错误 - 忘记清理
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
// 忘记调用 stop()
6. 多次 Notify 的效果
// 多次调用 Notify 会扩展信号集
sigChan1 := make(chan os.Signal, 1)
sigChan2 := make(chan os.Signal, 1)
signal.Notify(sigChan1, os.Interrupt)
signal.Notify(sigChan2, os.Interrupt)
// 两个通道都会收到 SIGINT
7. Stop 保证
signal.Stop(sigChan)
// 当 Stop 返回时,保证 sigChan 不会再收到信号
8. Windows 特殊性
// Windows 上:
// - ^C (Control-C) 或 ^BREAK (Control-Break) 通常导致退出
// - 如果使用 Notify 订阅 os.Interrupt,会发送到通道而不退出
// - CTRL_CLOSE_EVENT、CTRL_LOGOFF_EVENT、CTRL_SHUTDOWN_EVENT 返回 SIGTERM
最后更新: 2026-04-05
Go 版本: Go 1.0+(NotifyContext 为 Go 1.16+)
包文档: https://pkg.go.dev/os/signal
相关包: os, context, syscall, time
相关文档: 信号处理最佳实践