Go服务必须同时监听os.Interrupt和syscall.SIGTERM才能实现优雅停机,因本地开发用Ctrl+C触发SIGINT,而Kubernetes等环境发SIGTERM;仅监听其一将导致部分场景硬退出。

Go 服务不监听 SIGTERM 和 SIGINT,就谈不上“优雅停机”——它会在收到信号后直接退出,defer 不执行、连接被强制断开、数据库事务中断、gRPC 流截断,等同于拔电源。
为什么必须同时监听 os.Interrupt 和 syscall.SIGTERM
本地开发按 Ctrl+C 触发的是 os.Interrupt(即 SIGINT),而 Kubernetes、systemd 或 docker stop 发送的是 syscall.SIGTERM。只监听其中一个,服务在一半环境里会硬退出,根本走不到清理逻辑。
-
signal.Notify必须显式列出两个信号:signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) - 别用
syscall.SIGHUP或SIGUSR1替代——它们语义是重载配置,不是关机 - Windows 不支持
SIGTERM,靠os.Interrupt就够,但跨平台代码仍建议两者都写,Go 会自动忽略不支持的信号
signal.Notify 的 channel 为什么必须带缓冲
无缓冲 channel 在信号到达时若无人接收,信号就丢了。比如快速连按两次 Ctrl+C,或 K8s 在滚动更新时并发发多个 SIGTERM,没缓冲就只收一个,第二次直接触发默认退出行为。
- 必须用
make(chan os.Signal, 1),缓冲大小为 1 即可,多了反而掩盖问题 - 不能把
signal.Notify丢进 goroutine 就不管——主 goroutine 必须显式阻塞等待,例如或 <code>for range sigChan - 注册信号监听必须在
http.ListenAndServe或其他阻塞调用之前,否则信号可能在监听建立前就已到达并被系统默认处理
http.Server.Shutdown() 为什么不能直接用 context.Background()
Shutdown() 本身不设超时,全靠传入的 context.Context 控制。传 context.Background() 或没设 deadline 的 context.WithCancel(),等于让服务无限等待卡死。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- K8s 默认
terminationGracePeriodSeconds: 30,你的context.WithTimeout(ctx, 25*time.Second)必须比它小至少 5 秒,否则 K8s 强杀前清理根本跑不完 -
Shutdown()返回context.DeadlineExceeded是常见现象,说明有请求卡住——此时记录日志即可,不必 panic,更不能 fallback 到Close() - 调完
Shutdown()后必须立刻调cancel(),否则 context 泄漏,goroutine 持续监听ctx.Done()
哪些资源必须手动关,且顺序不能错
http.Server.Shutdown() 只管 HTTP 连接层,gRPC、*sql.DB、Redis、WebSocket、定时器、后台 goroutine 全得你亲手关。顺序错了轻则 panic,重则数据丢失。
- 典型关闭顺序:先停消息消费者(如 Kafka),再等 DB 连接池空闲(可先
db.SetMaxOpenConns(1)加速归还),最后关 HTTP server -
*sql.DB.Close()是阻塞操作,要确保此前已无新查询发起,否则会一直等;更稳妥的是先拦截新请求(中间件开关),再等存量完成 - gRPC server 必须用
grpcServer.GracefulStop(),不是Stop();它不等流式 RPC 完成,需配合业务层超时 - WebSocket 连接不会被
Shutdown()自动等待,得自己维护sync.Map存活连接,收到 shutdown 信号后遍历调conn.Close()
最容易被忽略的点是:信号监听只是起点,真正决定是否“优雅”的,是你有没有让所有 goroutine 都感知同一个 ctx.Done(),以及有没有给每个外部依赖加 timeout。没有统一上下文驱动的清理,再多的 Shutdown() 调用也只是表面功夫。

















