Go 程序用 os/signal 包的 signal.Notify 将 SIGUSR1 等信号转为 channel 值,需配合 goroutine 持续接收,否则信号会丢失。

Go 程序怎么捕获 SIGUSR1 这类信号
Go 用 os/signal 包监听信号,核心是 signal.Notify。它把操作系统发来的信号转成 Go channel 中的值,而不是靠传统 signal handler 函数回调——这点和 C/Python 不同,得适应。
常见错误是只监听一次就退出,或者没开 goroutine 持续接收,导致信号“丢失”:
-
signal.Notify只设置监听规则,不自动消费信号;必须用for range或显式读取 - 监听后不启动 goroutine 处理,主 goroutine 结束程序就退出,信号来不及送达
- 误用
syscall.SIGINT和os.Interrupt:后者在 Windows 上是os.Kill,跨平台建议统一用os.Interrupt
示例片段(仅捕获,不转发):
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGUSR1, os.Interrupt)
go func() {
for sig := range sigCh {
log.Printf("received %v", sig)
}
}()
子进程启动时怎么让信号能传过去
关键不是“转发”,而是让子进程继承父进程的信号行为。默认情况下,Go 启动的子进程(exec.Command)会继承父进程的信号 mask 和处理方式,但前提是父进程没主动屏蔽或忽略信号。
立即学习“go语言免费学习笔记(深入)”;
容易踩的坑:
- 父进程调用
signal.Ignore后,子进程仍可能收到该信号(取决于系统),但行为不可控;应避免 Ignore,改用 Notify + 主动传递 - 子进程是 shell 脚本时,需确保脚本没用
trap '' SIGUSR1之类屏蔽掉信号 - Linux 下,如果子进程是孤儿进程(父退出),init 进程会接管,信号可能被吞掉;所以转发逻辑必须在父进程生命周期内完成
正确做法:启动子进程前,确保信号未被忽略,且子进程可响应:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
cmd := exec.Command("sh", "-c", "trap 'echo got USR1' USR1; sleep 100")
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true, // 避免和父进程共用进程组,方便后续精准 kill
}
err := cmd.Start()
怎么把 SIGUSR1 安全地发给子进程
不能简单用 cmd.Process.Signal 直接发——如果子进程已退出、PID 被复用,信号可能误发给其他进程。必须确认子进程还活着,且 PID 有效。
实操要点:
- 用
cmd.Process.Pid获取 PID,再调用syscall.Kill(pid, syscall.SIGUSR1),比cmd.Process.Signal更可控(后者内部也走 Kill) - 发信号前先检查
cmd.ProcessState == nil且kill -0 $pid成功(可用syscall.Kill(pid, 0)模拟) - 若子进程是多线程程序,
SIGUSR1默认只发给主线程;需要子进程自己用pthread_sigmask或 Go 的runtime.LockOSThread配合处理
简短判断+发送逻辑:
if cmd.Process != nil {
if err := syscall.Kill(cmd.Process.Pid, syscall.SIGUSR1); err != nil {
log.Printf("failed to send SIGUSR1 to %d: %v", cmd.Process.Pid, err)
}
}
为什么 signal.Stop 和 signal.Reset 很少用到
这两个函数用于清理 signal.Notify 注册的 channel,但实际项目里几乎不用:因为 signal channel 通常伴随整个程序生命周期,没必要中途取消;而频繁注册/注销反而容易漏信号或引发 panic。
真正要小心的是资源泄漏场景:
- 一个 long-running server 启动多个子命令,每次监听都 new channel 却不 stop,会导致 goroutine 泄漏
- 测试中反复启停信号监听,没 reset 可能影响后续测试(比如上个测试把
SIGINTignore 了,下一个测试收不到) -
signal.Reset会恢复系统默认行为(终止/忽略),如果之前用Notify接管过,重置后信号可能直接杀掉进程
结论:除非你在写 CLI 工具的子命令切换逻辑,或者做高密度信号测试,否则别碰 Reset 和 Stop。监听一次,用到程序结束,最稳。
信号转发真正的复杂点不在 Go 语法,而在父子进程的生命周期对齐、PID 有效性验证、以及子进程自身是否真的注册并响应了那个信号——这些都得靠日志和 strace -p $pid -e trace=signal 来交叉验证。

















