Go服务优雅退出需同时处理信号监听、HTTP/gRPC关闭、服务反注册及goroutine清理:用signal.Notify监听SIGINT/SIGTERM,Shutdown和GracefulStop并行执行并设超时,反注册必须在Shutdown后os.Exit前完成,所有后台goroutine须共享同一context并由WaitGroup管理。

Go 服务要真正不丢请求地退出,只调 http.Server.Shutdown() 是不够的——它不监听信号、不等 gRPC 完成、不反注册 Consul、也不管后台 goroutine 是否还在跑。漏掉任意一环,就会出现连接 RST、幽灵实例或重复消费。
signal.Notify 必须只监听 SIGTERM 和 SIGINT 且仅一次
容器环境(如 K8s)发 SIGTERM,本地调试按 Ctrl+C 发 SIGINT。两者都必须响应,否则对应场景下服务完全“收不到下线指令”。
-
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)是唯一安全写法;用os.Interrupt在某些容器里会失效 - channel 缓冲大小设为 1:
sigChan := make(chan os.Signal, 1);缓冲太大可能积压多个信号,导致多次触发 shutdown - 必须在主 goroutine 里调用且只调一次;重复注册会让 channel 阻塞或丢信号
- 注册后必须显式读取:
<-sigChan;否则主 goroutine 退出,信号无处落脚
http.Server.Shutdown() 必须带超时 context 且放 goroutine 里执行
Shutdown() 是阻塞调用,不加超时会卡死,不放 goroutine 里则后续所有清理逻辑(gRPC 关闭、DB 反注册)根本不会执行。
- 超时 context 正确写法:
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second);不能用context.TODO()或裸context.Background() -
cancel()不能defer:否则刚建完就取消,Shutdown()立刻返回context.Canceled - 必须检查返回 error:
err != nil && err != http.ErrServerClosed才需 log;http.ErrServerClosed是正常退出标志 -
Shutdown()不等 handler 主动退出——它只停止 accept,等待连接自然结束;handler 内部必须响应req.Context().Done(),比如用db.QueryContext(ctx, sql)
gRPC Server 必须用 GracefulStop(),不能用 Stop()
Stop() 等价于 kill -9,会立刻断开所有连接;GracefulStop() 才是真正等待已有请求完成的优雅方式。
立即学习“go语言免费学习笔记(深入)”;
- 必须和 HTTP shutdown 并行执行,不能串行:否则总超时时间会被拉长
- 推荐用
sync.WaitGroup协同:wg.Add(2),一个 goroutine 调httpServer.Shutdown(),另一个调grpcServer.GracefulStop() - 别忘了
grpcServer.GracefulStop()本身也阻塞,同样需要放在 goroutine 里,避免卡住主流程
反注册和资源清理必须严格排序且容错
Consul/Etcd 反注册不是“收尾可选动作”,而是不可跳过、不可乱序、不可静默失败的关键环节。
- 反注册必须在
httpServer.Shutdown()返回之后、os.Exit(0)之前执行;早了流量断,晚了进程已死,请求打到空壳上 - 不能放
defer:panic 时不会执行,导致幽灵实例残留 -
*sql.DB.Close()是阻塞的,但它只等 idle 连接归还,不等正在执行的 query —— 所以必须确保所有 handler 已进入 shutdown 流程,不再发起新 query - Kafka 消费者(如 sarama)需调
consumer.Close(),并确认ConsumePartition循环已退出;否则可能 panic 或重复消费
最易被忽略的点是:所有后台 goroutine 必须通过同一个 context.Context 控制生命周期,且每个启动时 wg.Add(1)、退出前 wg.Done();否则 Shutdown() 返回后,goroutine 还在跑,服务“看似退出”实则泄漏。


















