go框架不内置信号处理,必须显式调用signal.notify注册syscall.sigint和syscall.sigterm,且须在主业务(如http服务)启动前完成;通道需带缓冲并持续读取,收到信号后应触发优雅关闭而非直接os.exit。

Go 框架本身不内置信号处理逻辑,所有信号响应都得你自己用 signal.Notify 显式注册;没这一步,SIGINT 和 SIGTERM 会直接终止进程,defer、http.Server.Shutdown 全部跳过。
signal.Notify 必须在主业务启动前调用
常见错误是把 signal.Notify 放在 http.ListenAndServe 或长期 goroutine 启动之后。信号可能在注册前就已到达,内核直接丢弃,程序“看似没反应”。
- 正确顺序:先创建带缓冲 channel → 调用
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)→ 再启动 HTTP server 或其他长期任务 - 别只监听
os.Interrupt:它只是SIGINT的别名,SIGTERM(如kill -15、Kubernetes 删除 pod)必须显式加进去 - 容器或 systemd 环境下,还需加
syscall.SIGHUP,否则 reload 配置失败
通道必须带缓冲且持续读取
make(chan os.Signal, 1) 是底线;无缓冲 channel 在第一次信号到来时就会阻塞,后续信号全丢——比如连按两次 Ctrl+C,第二次收不到。
- 必须用
for sig := range sigChan循环消费,不能只读一次 - 缓冲设为 1 足够:信号是事件通知,不是数据流;设更大(如 10)反而掩盖问题,比如 goroutine 挂了没人读 channel,信号越积越多
- channel 生命周期要和进程一致:不能在子 goroutine 里创建并监听,否则 main 退出后整个监听失效
收到信号后只该发通知,别做清理
在 signal handler 里调 os.Exit、log.Fatal 或阻塞操作(如等 DB 连接池归还),会导致进程僵住、资源泄漏、日志卡死。
- 推荐做法:用
done chan struct{}或atomic.StoreInt32(&shutdownFlag, 1)仅作标记 - 主 goroutine 主动检查该标记,调
srv.Shutdown()、db.Close()、wg.Wait(),再自然 return - 所有长期 goroutine 必须接收
context.Context,并在ctx.Done()后自行退出
子进程信号转发需进程组隔离
直接用 exec.Command 启动子进程,cmd.Process.Signal(syscall.SIGTERM) 只杀主进程,shell 脚本里的子命令(如 curl、sleep)会变成孤儿继续跑。
- Linux/macOS:必须设
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} - Windows:改用
cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP} - 发信号时用
syscall.Kill(-pgid, syscall.SIGTERM)(注意负号),确保整组收到 - Windows 下
SIGTERM会 panic,得分支处理:用taskkill /PID %d /T /F杀进程树
真正难的不是注册信号,而是让所有 goroutine 和子进程同步退出——HTTP handler 里一个没加 ctx 的 http.Client.Do,就能让 Shutdown 卡满超时时间。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











