Go 应用程序如何正确捕获并持续处理系统信号

go 程序需通过循环读取信号通道才能持续响应 `kill` 命令发送的各类信号(如 sigterm、sigint),否则仅首次信号(如 ctrl+c)生效,后续信号将被忽略。

在 Go 中,os/signal 包用于监听操作系统信号,但其行为高度依赖通道消费方式。原始代码中,goroutine 仅执行一次 igs 操作后即退出,导致信号通道后续接收的信号无人消费——这些信号虽已成功发送并被内核投递到 Go 运行时,却因通道缓冲区满(容量为 1)且无消费者而被静默丢弃。

正确做法是使用 for 循环持续从信号通道读取,确保每次信号到达都能被及时处理:

package main

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"
)

func main() {
    sigs := make(chan os.Signal, 1)
    done := make(chan bool, 1)

    // 监听常见终止类信号;可显式指定以提高可读性
    signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGUSR1)

    go func() {
        for {
            sig := <-sigs
            fmt.Printf("Received signal: %v\n", sig)
            // 可在此处添加优雅退出逻辑,如关闭连接、保存状态等

if sig == syscall.SIGTERM || sig == syscall.SIGINT { fmt.Println("Shutting down gracefully...") done <- true return } } }() fmt.Println("Application running. Send signals with: kill - ") <-done fmt.Println("Exiting cleanly.") }

关键注意事项:

  • signal.Notify(sigs) 若不传入具体信号,默认监听所有可捕获的同步信号(含 SIGUSR1、SIGUSR2 等),但不包括 SIGKILL(9)和 SIGSTOP(19)——二者无法被捕获或忽略,由内核强制执行;
  • 通道缓冲区大小设为 1 足够应对大多数场景,但若需防止高频率信号丢失,可适当增大(如 make(chan os.Signal, 10)),并配合 select + default 实现非阻塞消费;
  • 生产环境应避免仅依赖 fmt.Println 做信号响应,建议集成日志库、触发上下文取消(context.WithCancel)、执行资源清理,并设置超时强制退出;
  • 使用 syscall 显式指定信号(如 syscall.SIGTERM)比依赖数字更安全、可移植且语义清晰。

通过上述改进,程序即可稳定响应 kill -15 $PID(SIGTERM)、kill -2 $PID(SIGINT)、kill -1 $PID(SIGHUP)等命令,实现符合 Unix 哲学的可靠信号处理机制。