生在红旗下 發表於 2025-12-24 11:23:46

Golang信号处理实战

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">1. 为什么需要信号处理</a></li><li><a href="#_label1">2. 核心 API</a></li><li><a href="#_label2">3. 基本使用</a></li><ul class="second_class_ul"><li><a href="#_lab2_2_0">示例:监听SIGINT(Ctrl+C)和SIGTERM(kill)</a></li></ul><li><a href="#_label3">4. 使用NotifyContext优雅退出</a></li><ul class="second_class_ul"></ul><li><a href="#_label4">5. 高级用法</a></li><ul class="second_class_ul"><li><a href="#_lab2_4_1">5.1 忽略信号</a></li><li><a href="#_lab2_4_2">5.2 动态取消订阅</a></li><li><a href="#_lab2_4_3">5.3 同时监听多个信号</a></li></ul><li><a href="#_label5">6. 原理机制</a></li><ul class="second_class_ul"></ul><li><a href="#_label6">7. 最佳实践</a></li><ul class="second_class_ul"></ul><li><a href="#_label7">8. 实战案例:优雅关闭 HTTP 服务器</a></li><ul class="second_class_ul"></ul></ul></div><p class="maodian"><a name="_label0"></a></p><h2>1. 为什么需要信号处理</h2>
<p>在类 Unix 系统中,信号(Signal)是一种<strong>异步通知机制</strong>,内核通过它告诉进程发生了某种事件,比如:</p>
<ul><li><strong>终止进程</strong>:<code>SIGTERM</code>(kill 发送的默认信号)、<code>SIGINT</code>(Ctrl+C)</li><li><strong>挂起/恢复</strong>:<code>SIGTSTP</code>(Ctrl+Z)</li><li><strong>重新加载配置</strong>:<code>SIGHUP</code></li><li><strong>自定义信号</strong>:<code>SIGUSR1</code>、<code>SIGUSR2</code></li></ul>
<p>如果不处理,进程会使用 <strong>默认行为</strong>(可能直接退出)。<br />而 <code>os/signal</code> 包让我们在用户态捕获这些信号,并执行自定义逻辑(比如优雅退出、保存状态、重载配置等)。</p>
<p class="maodian"><a name="_label1"></a></p><h2>2. 核心 API</h2>
<table><thead><tr><th>函数</th><th>功能</th><th>常见用途</th></tr></thead><tbody><tr><td>Notify(c chan&lt;- os.Signal, sig ...os.Signal)</td><td>将指定信号转发到 c</td><td>订阅信号</td></tr><tr><td>Stop(c chan&lt;- os.Signal)</td><td>停止向 c 转发信号</td><td>取消订阅</td></tr><tr><td>Ignore(sig ...os.Signal)</td><td>忽略信号(不再转发给程序)</td><td>屏蔽特定信号</td></tr><tr><td>Reset(sig ...os.Signal)</td><td>恢复信号默认行为</td><td>信号处理恢复默认</td></tr><tr><td>NotifyContext(ctx, sig...)</td><td>返回会在收到信号时自动 cancel 的 Context</td><td>优雅退出</td></tr></tbody></table>
<p class="maodian"><a name="_label2"></a></p><h2>3. 基本使用</h2>
<p class="maodian"><a name="_lab2_2_0"></a></p><h3>示例:监听SIGINT(Ctrl+C)和SIGTERM(kill)</h3>
<div class="jb51code"><pre class="brush:go;">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 := &lt;-sigChan // 阻塞等待
        fmt.Println("收到信号:", sig)

        fmt.Println("执行清理逻辑...")
        // 这里做关闭文件、断开连接等操作

        fmt.Println("程序退出")
}
</pre></div>
<p>运行:</p>
<div class="jb51code"><pre class="brush:go;">go run main.go
# Ctrl+C 或 kill PID 会触发信号
</pre></div>
<p class="maodian"><a name="_label3"></a></p><h2>4. 使用NotifyContext优雅退出</h2>
<p>Go 1.16+ 引入的 <code>NotifyContext</code> 结合 <code>context</code> 让信号处理更简洁。</p>
<div class="jb51code"><pre class="brush:go;">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 &lt;-ctx.Done():
                                fmt.Println("业务收到退出信号,清理中...")
                                time.Sleep(1 * time.Second)
                                fmt.Println("业务清理完成")
                                return
                        default:
                                fmt.Println("业务运行中...")
                                time.Sleep(2 * time.Second)
                        }
                }
        }()

        &lt;-ctx.Done() // 阻塞,直到信号触发
        fmt.Println("主程序退出")
}
</pre></div>
<p>好处:</p>
<ul><li>自动取消 context</li><li>不用自己建 channel</li><li>多个 goroutine 可同时感知退出</li></ul>
<p class="maodian"><a name="_label4"></a></p><h2>5. 高级用法</h2>
<p class="maodian"><a name="_lab2_4_1"></a></p><h3>5.1 忽略信号</h3>
<div class="jb51code"><pre class="brush:go;">signal.Ignore(syscall.SIGPIPE) // 忽略管道断开
</pre></div>
<p class="maodian"><a name="_lab2_4_2"></a></p><h3>5.2 动态取消订阅</h3>
<div class="jb51code"><pre class="brush:go;">signal.Stop(sigChan) // 取消 channel 的订阅
</pre></div>
<p class="maodian"><a name="_lab2_4_3"></a></p><h3>5.3 同时监听多个信号</h3>
<div class="jb51code"><pre class="brush:go;">signal.Notify(sigChan) // 不指定信号时,监听所有信号
</pre></div>
<blockquote><p>不推荐监听全部信号,可能会拦截 SIGKILL、SIGSTOP 等无法处理的信号。</p></blockquote>
<p class="maodian"><a name="_label5"></a></p><h2>6. 原理机制</h2>
<p>简化版流程:</p>
<ol><li><code>Notify</code> 注册信号 &rarr; 调用 runtime 的 <code>enableSignal(n)</code>。</li><li>runtime 捕获信号后调用 <code>process()</code>。</li><li><code>process</code> 遍历所有 channel handler,非阻塞发送信号。</li><li><code>Stop</code> 时调用 <code>disableSignal(n)</code>,等待 runtime 信号队列清空(<code>signalWaitUntilIdle()</code>)。</li></ol>
<p>特点:</p>
<ul><li><strong>非阻塞投递</strong>:channel 必须有缓冲,否则可能丢信号。</li><li><strong>引用计数</strong>:多个 channel 可监听同一信号,ref=0 时才会真正停止捕获。</li><li><strong>bitmask 存储</strong>:handler 用 bit 位记录关注的信号,内存占用小。</li></ul>
<p class="maodian"><a name="_label6"></a></p><h2>7. 最佳实践</h2>
<ol><li><p><strong>总是用缓冲 channel</strong></p>
<div class="jb51code"><pre class="brush:go;">make(chan os.Signal, 1)
</pre></div>
<p>避免信号丢失。</p></li><li><p><strong>优雅退出而不是强杀</strong><br />在 <code>SIGTERM</code> 里做清理,配合 <code>context</code> 实现安全收尾。</p></li><li><p><strong>避免监听全部信号</strong><br />只订阅需要的信号,避免影响系统默认行为。</p></li><li><p><strong>多 goroutine 协同</strong><br />用 <code>NotifyContext</code> 让所有协程通过 <code>&lt;-ctx.Done()</code> 感知退出。</p></li><li><p><strong>容器化部署必备</strong><br />Docker 默认用 <code>SIGTERM</code> 停止容器,业务代码应处理此信号。</p></li></ol>
<p class="maodian"><a name="_label7"></a></p><h2>8. 实战案例:优雅关闭 HTTP 服务器</h2>
<div class="jb51code"><pre class="brush:go;">package main

import (
        "context"
        "fmt"
        "net/http"
        "os/signal"
        "syscall"
        "time"
)

func main() {
        srv := &amp;http.Server{Addr: ":8080"}

        // 启动 HTTP 服务
        go func() {
                fmt.Println("HTTP 服务启动在 :8080")
                if err := srv.ListenAndServe(); err != nil &amp;&amp; err != http.ErrServerClosed {
                        fmt.Println("HTTP 服务器出错:", err)
                }
        }()

        // 信号监听
        ctx, stop := signal.NotifyContext(context.Background(),
                syscall.SIGINT, syscall.SIGTERM)
        defer stop()

        &lt;-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("服务器已优雅退出")
}
</pre></div>
<p>这样写的好处:</p>
<ul><li>支持 Ctrl+C / kill</li><li>容器化部署时能优雅退出</li><li>确保连接处理完成后再关闭</li></ul>
頁: [1]
查看完整版本: Golang信号处理实战