signal.Notify 是 Go 中唯一可靠且线程安全的信号监听机制,它将操作系统信号转发至 channel 供主动接收,需配合缓冲大小为 1 的 channel、主 goroutine 中循环接收,并注意容器中 PID 1 导致 SIGTERM 无法送达的问题。

Go 里用 signal.Notify 捕获中断信号是唯一可靠方式
Go 没有传统意义上的“信号函数”(比如 C 的 signal() 或 Python 的 signal.signal()),signal.Notify 是标准库提供的、唯一被推荐且线程安全的信号监听机制。它本质是把操作系统信号转发到 Go channel,由你主动接收处理,而不是注册回调函数。
常见错误是试图用 syscall.Signal 常量直接传给某个不存在的“注册函数”,或误以为 os.Interrupt 本身能触发监听——它只是个常量,必须配合 signal.Notify 才生效。
-
signal.Notify第一个参数必须是带缓冲或无缓冲的chan os.Signal,不能是普通变量或未初始化 channel - 必须在主 goroutine 中启动接收循环(如
for range ch),否则信号来了就丢 - 监听
os.Interrupt(即 Ctrl+C)和syscall.SIGTERM覆盖绝大多数场景;不要盲目加syscall.SIGKILL——它无法被捕获
为什么 signal.Ignore 和 signal.Stop 容易用错
signal.Ignore 用于显式忽略某信号(例如让程序对 SIGHUP 完全无感),但它只影响当前进程,且一旦调用就不能撤销;而 signal.Stop 是停止往指定 channel 发送信号,但不会清空 channel 中已排队的信号,也不影响其他 channel。
典型误用:在收到一次 os.Interrupt 后立刻调用 signal.Stop(ch),以为能“取消监听”,结果下一次 Ctrl+C 还是会触发——因为 Stop 不关闭 channel,老的接收循环仍在运行。
立即学习“go语言免费学习笔记(深入)”;
- 想“临时停用”监听?直接关闭 channel 并重置接收逻辑,别依赖
Stop -
signal.Ignore(syscall.SIGPIPE)在网络服务中很常见,避免因管道破裂导致 panic,但注意它会影响整个进程,子进程继承该设置 - 多个
signal.Notify调用指向同一个 channel 是安全的;指向不同 channel 则信号会广播过去
signal.Notify 的缓冲区大小选 1 还是 2?
缓冲区大小决定能“攒”几个未处理信号。设为 1 是最常用选择,够应付快速连按 Ctrl+C;设为 0(无缓冲)则要求你每次信号一来就必须立刻接收,否则后续信号会被丢弃(Go runtime 会静默丢弃)。
实际中,除非你明确要做“信号节流”(比如只响应第一次 SIGTERM,后续忽略),否则别用 0。更大的缓冲(如 5)意义不大——多数运维场景只发一次 SIGTERM,等你清理完再发 SIGKILL。
- 推荐写法:
sigCh := make(chan os.Signal, 1) - 如果监听多个信号(如
os.Interrupt,syscall.SIGTERM,syscall.SIGHUP),仍用 size=1 即可,因为它们极少同时到达 - 切记:channel 缓冲区不解决“处理慢”的问题——真正耗时的操作(如关数据库连接)必须放 goroutine 里做,主 channel 接收要快
容器环境里 SIGTERM 收不到?检查 init 进程和 PID namespace
在 Docker/Kubernetes 中,Go 程序常作为 PID 1 运行。若没用 tini 或 docker run --init,操作系统发来的 SIGTERM 可能根本传不到你的程序——因为 PID 1 对信号的默认处理行为特殊(比如忽略某些信号),且没有 init 进程帮忙转发。
现象是:本地 kill -TERM $(pidof myapp) 正常触发,但 docker stop 或 kubectl delete pod 后程序直接被杀,log 里看不到任何信号处理日志。
- 验证方式:进容器执行
cat /proc/1/status | grep Tgid,若 Tgid 不是你程序的 PID,说明不是 PID 1;若是,就得加 init - Docker 启动加
--init参数,或镜像里用FROM golang:alpine AS builder+RUN apk add --no-cache tini+ENTRYPOINT ["/sbin/tini", "--"] - K8s 中可在 Pod spec 加
securityContext: {runAsUser: 0}和initContainers预装 tini,但更简单的是用docker run --init构建的镜像直接部署
信号监听本身很简单,难的是在不同部署环境里确保信号真能送达、不被截断、不被忽略。尤其当程序跑在容器或 systemd 下时,操作系统层的信号路由比 Go 代码本身更值得花时间排查。


















