Go语言os.Signal监听系统信号实现优雅关闭
在Go语言开发的服务端程序中,如果直接强制终止进程(如直接使用 kill -9 或关闭终端窗口),正在处理的请求可能会突然中断,导致数据不一致或文件损坏。为了避免这种情况,我们需要实现“优雅关闭”:程序收到退出信号后,先停止接收新请求,处理完已接收的请求,然后再释放资源并退出。
核心流程与原理
实现优雅关闭的核心在于利用 Go 标准库中的 os/signal 和 syscall 包来捕获操作系统发送给进程的信号(如 Ctrl+C 发送的 SIGINT),并结合 net/http 库的 Shutdown 方法来平滑关闭服务。
以下是信号处理与关闭的完整逻辑流向:
具体实施步骤
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) 方法。该方法会完成以下动作:
- 立即停止监听新的网络连接。
- 关闭所有空闲的连接。
- 无限期等待活跃连接处理完成,直到超时。
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("服务器已优雅退出")
}
验证与测试
执行以下操作来验证优雅关闭是否生效。
-
运行程序:
在终端中输入命令启动服务。go run main.go -
模拟并发请求:
打开一个新的终端窗口,使用curl发送一个请求,该请求会因代码中的Sleep而持续 2 秒。curl http://localhost:8080/ -
触发关闭信号:
在运行服务的第一个终端窗口中,立即按下Ctrl + C。
观察输出日志:
- 如果实现正确,你会看到日志显示“收到退出信号”,随后显示“正在关闭服务器”。
- 即使你按下了
Ctrl+C,刚才那个curl请求依然会等待 2 秒并成功返回 "Hello World!",而不是报错连接重置。 - 5秒后,程序会自动彻底退出。
常见信号说明
在 Linux/Unix 系统中,我们通常处理以下两个信号。
| 信号名称 | 数值 | 对应操作 | 说明 |
|---|---|---|---|
SIGINT |
2 | Ctrl + C |
键盘中断信号,用于通知前台进程组终止进程。 |
SIGTERM |
15 | kill <pid> |
程序结束信号,可以被进程捕获并处理,是 kill 命令默认发送的信号。 |
注意:SIGKILL (信号 9) 是无法被捕获和忽略的,它会强制杀死进程,因此实现优雅关闭时不应依赖该信号。

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