Go中signal.Notify必须使用chan os.Signal类型通道,容量至少为1,否则panic或丢信号;需配合goroutine持续读取并手动处理退出逻辑,主goroutine不可直接return。

Go 中用 signal.Notify 接收信号必须配 os.Signal 类型通道
直接传 chan int 或 chan string 会 panic,因为 signal.Notify 要求通道元素类型严格为 os.Signal。常见错误是随手写 make(chan int, 1),运行时立刻报 panic: signal: unsupported channel type。
正确做法是:
sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
- 通道容量至少为 1,否则首次信号可能丢失(尤其在主 goroutine 还没来得及
时) - 多个信号可一次注册,不用反复调用
Notify - 若需监听所有可捕获信号(不推荐生产环境),可用
os.Interrupt, syscall.SIGTERM等显式列出,避免用os.Kill或syscall.SIGKILL—— 它们无法被 Go 捕获
阻塞等待信号时别卡死主线程,要用 goroutine + select
如果在 main() 函数里直接写 ,程序就停在这儿了,其他逻辑(比如 HTTP server 启动、定时任务初始化)根本没机会执行。
典型安全模式是:
go func() {
sig := // 启动服务、启动协程、初始化……
http.ListenAndServe(":8080", nil)
- 必须用
go func()启动监听,否则阻塞 - 收到信号后建议调用
os.Exit(0)明确退出,避免残留 goroutine 导致进程 hang 住 - 不要在信号处理函数里做耗时操作(如同步写磁盘、远程调用),应发通知给主逻辑或用带超时的 context 控制
优雅退出的关键是「可中断」+ 「有超时」,不是只关监听器
仅关闭信号通道或停止 signal.Notify 没用。真正要退出的是你正在跑的服务、数据库连接、长轮询 goroutine 等。这些组件本身得支持中断。
例如 HTTP server 关闭需配合 context:
srv := &http.Server{Addr: ":8080", Handler: handler}
go func() {
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}()
<p></p>
-
srv.Shutdown()是 Go 1.8+ 提供的标准优雅关闭方式,依赖context控制超时 - 自定义 goroutine 需监听
ctx.Done(),不能靠全局变量或无条件 for 循环 - 数据库连接池、Redis client 等第三方库通常提供
Close()或Shutdown()方法,务必查阅文档并调用
测试信号处理逻辑不能真发 kill,得用 syscall.Kill + os.FindProcess
本地调试时手动 kill -TERM $(pidof myapp) 效率低且难自动化。单元测试中更不能依赖外部进程。
可靠方案是在测试里模拟发送信号:
func TestSignalHandling(t *testing.T) {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGTERM)
<pre class="brush:php;toolbar:false;">done := make(chan bool)
go func() {
<p>}
</p>
-
os.FindProcess(os.Getpid()).Signal()是唯一能在测试中可控触发信号的方式 - 别用
exec.Command("kill", ...),跨平台兼容差,且可能误杀其他进程 - 注意:Windows 对部分信号支持有限(如
SIGUSR1不可用),测试时优先用SIGINT或SIGTERM
信号处理本身很简单,难的是让整个程序的生命周期与信号对齐——每个活跃组件都得响应中断,每条路径都有超时兜底。漏掉一个 goroutine 或一个未关闭的连接,os.Exit() 就可能卡住。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











