在编写长期运行的服务(如 Web Server、消息消费者)时,优雅地处理中断信号(如 Ctrl+C 触发的 SIGINT)是保证数据一致性和服务稳定性的关键。

核心思路

在 Go 中,优雅处理信号的核心在于:将系统信号转化为 Context 的取消信号

  1. 监听信号:使用 os/signal 包监听 SIGINTSIGTERM
  2. 通知退出:通过 context.Context 通知所有 Goroutine 停止工作。
  3. 清理资源:等待正在进行的任务完成,关闭数据库连接、文件描述符等。

推荐做法:使用 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)

为什么需要优雅退出?

  1. 防止数据损坏:确保正在写入文件的操作或数据库事务完整结束。
  2. 释放外部资源:主动关闭连接,避免服务端出现大量 CLOSE_WAIT 状态。
  3. 负载均衡友好:如果是微服务,优雅退出可以让注册中心及时摘除节点,避免请求打到正在关闭的服务上。
最佳实践

始终为清理过程设置一个 Timeout。如果程序在 5-10 秒内还没退出,说明可能存在死锁或任务卡死,此时应强制退出。