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<- os.Signal, sig ...os.Signal)</td><td>将指定信号转发到 c</td><td>订阅信号</td></tr><tr><td>Stop(c chan<- 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 := <-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 <-ctx.Done():
fmt.Println("业务收到退出信号,清理中...")
time.Sleep(1 * time.Second)
fmt.Println("业务清理完成")
return
default:
fmt.Println("业务运行中...")
time.Sleep(2 * time.Second)
}
}
}()
<-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> 注册信号 → 调用 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><-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 := &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("服务器已优雅退出")
}
</pre></div>
<p>这样写的好处:</p>
<ul><li>支持 Ctrl+C / kill</li><li>容器化部署时能优雅退出</li><li>确保连接处理完成后再关闭</li></ul>
頁:
[1]