go程序需用signal.notify监听sigint和sigterm,通过带缓冲channel接收信号;收到后应触发优雅退出:关闭服务、通知goroutine停止、等待清理完成,而非直接os.exit。

Go 程序如何监听 SIGINT 和 SIGTERM
Go 本身不自动响应系统信号,必须显式用 signal.Notify 注册监听。最常用的是 SIGINT(Ctrl+C)和 SIGTERM(kill -15),两者都应捕获——前者用于本地调试中断,后者是容器/K8s 中的标准终止信号。
关键点:必须用带缓冲的 chan os.Signal,否则可能丢信号;且注册后要确保主 goroutine 不退出,否则程序直接终止,来不及执行清理逻辑。
常见错误:只监听 SIGINT 忽略 SIGTERM,导致在 Docker 或 systemd 下无法优雅停止。
示例片段:
立即学习“go语言免费学习笔记(深入)”;
sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) <-- 阻塞等待信号 <-- 收到后执行 cleanup()
为什么不能在 signal handler 里直接调用 os.Exit
os.Exit 会立即终止进程,跳过 defer、未完成的 goroutine、sync.WaitGroup 等所有收尾工作,等于放弃“优雅”二字。
正确做法是用一个全局 flag(如 shutdown bool 变量)或 channel 通知其他组件开始退出,并等待它们完成。
典型结构:
- 收到信号后,关闭监听 socket、设置 shutdown 标志、触发
context.WithCancel - 让 HTTP server 调用
srv.Shutdown(),而不是srv.Close() - 用
sync.WaitGroup等待所有长期 goroutine(如 worker、ticker)主动退出 - 最后才调用
os.Exit(0)或自然返回 main
HTTP Server 如何配合 context 实现平滑关闭
Go 1.8+ 的 http.Server.Shutdown() 是核心,但它依赖传入的 context.Context 控制超时。如果没设 timeout,它会无限等待活跃连接结束,拖慢退出速度。
常见陷阱:
- 忘记给
Shutdown()传 context,或传了context.Background()导致无超时 - 在
Shutdown()前就关闭 listener,导致新连接被拒但旧连接仍卡住 - 没设置
ReadTimeout/WriteTimeout,导致慢客户端拖死整个 shutdown 流程
推荐写法:
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("HTTP shutdown error: %v", err)
}goroutine 泄漏是优雅退出失败的最常见原因
很多服务启动了 ticker、worker pool、长轮询 goroutine,但没提供退出通道。一旦主流程想停,这些 goroutine 还在后台跑,导致程序 hang 住或资源泄漏。
解决方案统一:每个长期 goroutine 都要监听一个 done chan struct{} 或 context.Context.Done()。
例如:
go func() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
doWork()
case <-done: // 外部通知退出
return
}
}
}()注意:不要用 time.After 替代 ticker 做周期任务,它每次都会新建 timer,且无法主动 stop。
复杂点在于:有些第三方库(如某些 DB driver、gRPC client)内部也启了 goroutine,需查文档确认是否支持 context 取消或 Close 方法——漏掉一个,就可能卡住整个退出流程。










