go程序需用os/signal.notify监听sigterm,配合context.withcancel和http.server.shutdown实现优雅退出:先停listener,再等请求完成,最后清理资源;须避免信号丢失、并发shutdown、pid 1问题及长耗时操作无超时。

Go 程序怎么捕获 SIGTERM 实现优雅退出
直接上核心:用 os/signal.Notify 注册信号通道,配合 context.WithCancel 或显式关闭逻辑。别等主 goroutine 退出才处理——那时 HTTP server 可能已拒收新请求,但还在跑的 handler 却没机会收尾。
常见错误是只监听 SIGINT(Ctrl+C),线上环境发的是 SIGTERM;还有人把 signal.Notify 放在 goroutine 里却忘了阻塞主流程,导致程序秒退。
-
signal.Notify第二个参数必须显式列出要监听的信号,os.Interrupt不等于syscall.SIGTERM - 推荐用
make(chan os.Signal, 1),缓冲区为 1 防止信号丢失(比如快速连发两次SIGTERM) - 收到信号后,应先关闭 listener(如
http.Server.Shutdown),再等待活跃请求完成,最后退出
http.Server.Shutdown 为什么总超时失败
不是函数有问题,而是你没给它足够时间,或者没提前停止接收新连接。
Shutdown 只负责“不再接受新请求 + 等待已有 handler 返回”,它不会强制杀掉卡住的 goroutine。如果你的 handler 里有没设 timeout 的 http.Client.Do、数据库查询或 channel receive,就会拖垮整个关机流程。
立即学习“go语言免费学习笔记(深入)”;
- 调用
Shutdown前,确保已调用srv.Close()或类似操作停掉 listener(否则新连接仍可能进来) -
Shutdown的 context 要带合理超时,比如context.WithTimeout(ctx, 10*time.Second),别用context.background() - 检查所有长耗时操作是否都加了 context 控制,尤其是第三方库调用(如
redis.Client.Get、sql.DB.QueryRowContext)
多个服务共存时,如何统一信号响应逻辑
一个进程里跑 HTTP server、gRPC server、定时任务、后台 worker —— 不能每个都写一套 signal 处理。关键在于“信号只通知一次,退出动作可分层触发”。
典型踩坑是多个 goroutine 同时监听 SIGTERM,结果关了两遍 DB 连接,或 shutdown 被并发调用 panic。
- 全局只用一个
signal.Notify,把信号转发到一个中心chan os.Signal - 用
sync.Once包裹 shutdown 流程,确保无论哪个模块先响应,都只执行一次清理 - 各组件暴露
Close() Error方法,主 shutdown 函数按依赖顺序调用(比如先停 HTTP,再停 worker,最后关 DB) - 避免在 signal handler 里做重操作,只负责发通知、启 shutdown,具体逻辑放到单独 goroutine
linux 容器环境下 SIGTERM 收不到的常见原因
本地测试好好的,一上 docker 就“直接被 kill -9”,根本没走你的 shutdown 逻辑。大概率是进程没跑在 PID 1,或者容器启动方式绕过了信号转发。
Docker 默认把应用作为 PID 1 启动,但如果你用了 shell wrapper(如 ENTRYPOINT ["sh", "-c", "go run main.go"]),那真正跑 Go 的进程就不是 PID 1,SIGTERM 会被 sh 拦截,不透传。
- 确认 Go 进程确实是 PID 1:进容器执行
ps -o pid,comm,看输出第一行是不是你的二进制名 - 避免
sh -c启动,改用直接执行:ENTRYPOINT ["./myapp"] - kubernetes 中,确保没有设置
terminationGracePeriodSeconds: 0,否则会跳过 graceful shutdown 直接发SIGKILL - 某些 init 系统(如 tini)能帮你透传信号,可在 Dockerfile 中加入
RUN apt-get install -y tini并设为 entrypoint
最常被忽略的一点:信号处理本身不保证原子性。比如你在 shutdown 里关 DB 连接,同时另一个 goroutine 正在用这个连接发 query —— 没加锁或 context 取消,就会 panic。平滑关机不是加个 signal 监听就完事,得从 handler 层、client 层、资源层全链路对齐生命周期。