在编写长期运行的服务(如 Web Server、消息消费者)时,优雅地处理中断信号(如 Ctrl+C 触发的 SIGINT)是保证数据一致性和服务稳定性的关键。
核心思路
在 Go 中,优雅处理信号的核心在于:将系统信号转化为 Context 的取消信号。
- 监听信号:使用
os/signal包监听SIGINT或SIGTERM。 - 通知退出:通过
context.Context通知所有 Goroutine 停止工作。 - 清理资源:等待正在进行的任务完成,关闭数据库连接、文件描述符等。
推荐做法:使用 signal.NotifyContext (Go 1.16+)
这是目前最简洁、最推荐的方式。它会自动创建一个在接收到指定信号时被取消的 Context。
package main
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
// 1. 创建监听信号的 Context
// 当接收到 SIGINT (Ctrl+C) 或 SIGTERM 时,ctx.Done() 会被关闭
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop() // 释放资源
// 模拟业务逻辑
go func() {
for {
select {
case <-ctx.Done():
fmt.Println("Goroutine 收到停止信号,正在清理...")
return
default:
fmt.Println("正在处理业务...")
time.Sleep(1 * time.Sleep)
}
}
}()
// 2. 阻塞等待信号
<-ctx.Done()
fmt.Println("主进程收到中断信号,开始优雅退出...")
// 3. 执行清理工作(如关闭数据库、等待任务结束)
// 可以设置一个超时时间,防止清理工作卡死
_, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
fmt.Println("服务已安全关闭")
}传统做法:使用 Channel 监听
在 Go 1.16 之前,通常使用 chan os.Signal 手动处理。
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
// 阻塞直到收到信号
sig := <-sigChan
fmt.Printf("收到信号: %v\n", sig)为什么需要优雅退出?
- 防止数据损坏:确保正在写入文件的操作或数据库事务完整结束。
- 释放外部资源:主动关闭连接,避免服务端出现大量
CLOSE_WAIT状态。 - 负载均衡友好:如果是微服务,优雅退出可以让注册中心及时摘除节点,避免请求打到正在关闭的服务上。
最佳实践始终为清理过程设置一个 Timeout。如果程序在 5-10 秒内还没退出,说明可能存在死锁或任务卡死,此时应强制退出。