Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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
相关文档: 信号处理最佳实践