文章目录

Go语言os.Signal监听系统信号实现优雅关闭

发布于 2026-04-26 04:26:19 · 浏览 4 次 · 评论 0 条

Go语言os.Signal监听系统信号实现优雅关闭

在Go语言开发的服务端程序中,如果直接强制终止进程(如直接使用 kill -9 或关闭终端窗口),正在处理的请求可能会突然中断,导致数据不一致或文件损坏。为了避免这种情况,我们需要实现“优雅关闭”:程序收到退出信号后,先停止接收新请求,处理完已接收的请求,然后再释放资源并退出。


核心流程与原理

实现优雅关闭的核心在于利用 Go 标准库中的 os/signalsyscall 包来捕获操作系统发送给进程的信号(如 Ctrl+C 发送的 SIGINT),并结合 net/http 库的 Shutdown 方法来平滑关闭服务。

以下是信号处理与关闭的完整逻辑流向:

graph TD A["程序启动 main"] --> B["注册信号监听 signal.Notify"] B --> C["启动 HTTP 服务 Goroutine"] C --> D["循环处理网络请求"] A --> E["主 Goroutine 阻塞等待信号"] F["用户操作: Ctrl+C 或 kill 命令"] --> G["操作系统发送信号 SIGINT/SIGTERM"] G --> E E --> H["解除阻塞, 捕获到信号"] H --> I["创建超时上下文 Context"] I --> J["调用 http.Server Shutdown"] J --> K["Server 停止监听新端口"] K --> L["等待现有请求处理完成"] L --> M{"超时或处理完成?"} M -- "处理完成" --> N["正常退出 Exit 0"] M -- "超时" --> O["强制关闭并退出 Exit 1"]

具体实施步骤

1. 定义信号通道

创建一个无缓冲的通道 quit,用于专门接收操作系统发来的信号。这个通道的类型必须是 os.Signal

quit := make(chan os.Signal)

2. 注册监听信号

调用 signal.Notify 函数,将上面创建的通道 quit 与需要捕获的信号绑定。通常我们需要监听 SIGINT(中断信号,通常由 Ctrl+C 触发)和 SIGTERM(终止信号,通常由 kill 命令触发)。

signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)

3. 启动HTTP服务

初始化一个 http.Server 实例,并将其在一个独立的 Goroutine 中启动。这样主 Goroutine 才能空闲下来去执行上一步的信号监听阻塞操作。

编写路由处理逻辑,模拟耗时操作以便观察关闭效果。

srv := &http.Server{Addr: ":8080"}

go func() {
    // 启动服务,若发生错误则记录日志
    if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
        log.Fatalf("listen: %s\n", err)
    }
}()

4. 阻塞等待信号

在主 Goroutine 中,执行读取操作,从 quit 通道中获取数据。这行代码会阻塞程序的运行,直到通道中有信号传入。

sig := <-quit
log.Println("收到关闭信号:", sig)

5. 设置超时上下文

为了防止某些请求长时间挂起导致程序无法退出,我们需要创建一个带超时的 context.Context。通常设置为 5 秒或 10 秒,给正在处理的请求留出最后的收尾时间。

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

6. 执行优雅关闭

调用 srv.Shutdown(ctx) 方法。该方法会完成以下动作:

  1. 立即停止监听新的网络连接。
  2. 关闭所有空闲的连接。
  3. 无限期等待活跃连接处理完成,直到超时。
if err := srv.Shutdown(ctx); err != nil {
    log.Fatal("服务器强制关闭:", err)
}
log.Println("服务器已退出")

完整代码示例

将上述步骤整合,得到一个可直接运行的完整示例。保存main.go 并运行。

package main

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

func main() {
    // 1. 创建路由和处理函数
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        time.Sleep(2 * time.Second) // 模拟耗时业务处理
        w.Write([]byte("Hello World!"))
    })

    // 2. 配置服务器
    srv := &http.Server{Addr: ":8080"}

    // 3. 在协程中启动服务
    go func() {
        log.Println("服务启动,监听端口 :8080")
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("服务启动失败: %s\n", err)
        }
    }()

    // 4. 创建通道并监听信号
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)

    // 5. 阻塞等待信号
    sig := <-quit
    log.Println("收到退出信号:", sig)

    // 6. 创建超时上下文
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    // 7. 触发优雅关闭
    log.Println("正在关闭服务器...")
    if err := srv.Shutdown(ctx); err != nil {
        log.Fatal("服务器关闭异常:", err)
    }
    log.Println("服务器已优雅退出")
}

验证与测试

执行以下操作来验证优雅关闭是否生效。

  1. 运行程序:
    在终端中输入命令启动服务。

    go run main.go
  2. 模拟并发请求:
    打开一个新的终端窗口,使用 curl 发送一个请求,该请求会因代码中的 Sleep 而持续 2 秒。

    curl http://localhost:8080/
  3. 触发关闭信号:
    在运行服务的第一个终端窗口中,立即按下 Ctrl + C

观察输出日志:

  • 如果实现正确,你会看到日志显示“收到退出信号”,随后显示“正在关闭服务器”。
  • 即使你按下了 Ctrl+C,刚才那个 curl 请求依然会等待 2 秒并成功返回 "Hello World!",而不是报错连接重置。
  • 5秒后,程序会自动彻底退出。

常见信号说明

在 Linux/Unix 系统中,我们通常处理以下两个信号。

信号名称 数值 对应操作 说明
SIGINT 2 Ctrl + C 键盘中断信号,用于通知前台进程组终止进程。
SIGTERM 15 kill <pid> 程序结束信号,可以被进程捕获并处理,是 kill 命令默认发送的信号。

注意SIGKILL (信号 9) 是无法被捕获和忽略的,它会强制杀死进程,因此实现优雅关闭时不应依赖该信号。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文