go服务优雅下线需信号捕获+分层关闭+显式反注册:用signal.notify监听sigterm/sigint,shutdown()置goroutine中带超时执行,grpc/kafka/db/consul等须依次显式关闭,所有handler必须响应ctx.done()。

Go 服务下线不靠 os.Exit(0) 或 kill -9,靠的是信号捕获 + 资源分层关闭 + 显式反注册。跳过任意一环,就会出现幽灵实例、连接 RST、重复消费或请求丢失。
如何用 signal.Notify + http.Server.Shutdown 正确触发关闭流程
关键不是“有没有调 Shutdown()”,而是它是否在收到信号后立刻执行、是否带超时、是否不阻塞后续逻辑。
-
signal.Notify必须只注册一次,监听syscall.SIGTERM和syscall.SIGINT—— K8s preStop 发SIGTERM,本地调试用Ctrl+C是SIGINT,漏掉任一个都会导致无法响应 -
srv.Shutdown()必须放在 goroutine 里调用,否则主流程卡住,后续的 gRPC 关闭、DB 清理、Consul 反注册全不会执行 - 超时 context 不能
defer cancel():正确写法是ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second),cancel()留给Shutdown()内部或显式调用,否则刚建完就取消了 - 必须检查
Shutdown()返回 error:context.Canceled是正常(超时或主动 cancel),但其他 error(如网络异常、listener 已关)要log.Error,不能忽略
为什么仅 Shutdown HTTP Server 还远远不够
http.Server.Shutdown() 只管 HTTP 层:它停止 accept 新连接、等待活跃连接自然结束,但对 gRPC、Kafka 消费者、定时任务、WebSocket 管理器、后台 goroutine 完全无感。
- gRPC server 必须显式调
grpcServer.GracefulStop()——Stop()是立即断连,等同于kill -9 - Kafka 消费者(如 sarama)需调
consumer.Close(),并确保ConsumePartition循环已退出;否则可能 panic 或重复消费 - 所有后台 goroutine 必须通过
context.Context控制生命周期,启动时wg.Add(1),退出前wg.Done();Shutdown()前调wg.Wait() -
*sql.DB的db.Close()是阻塞的,但它只等 idle 连接归还,不等正在执行的 query —— 所以必须确保所有 handler 已进入 shutdown 流程,不再发起新 query
Consul 反注册的时机、重试与容错怎么写才安全
反注册不是“可有可无的收尾”,而是整个下线流程中不可跳过、不可乱序、不可静默失败的关键动作。顺序错了、没重试、没校验,就会留下幽灵实例。
- 反注册必须在
srv.Shutdown()返回之后、os.Exit(0)之前执行 —— 早了,流量断;晚了,进程已死,请求打到空壳上 - 不能放
defer:panic 或kill -9会跳过 defer,导致 deregister 根本不执行 - 必须带重试:建议最多 3 次,每次间隔 200ms,
client.Config.HttpClient.Timeout = 1500 * time.Millisecond - 反注册前先查
client.Agent().ServiceByID(serviceID):返回nil就跳过,避免 404 “Service not found” 日志刷屏 - 反注册成功后
time.Sleep(200 * time.Millisecond),给 Consul 集群广播状态留缓冲时间,降低下游缓存未刷新概率
handler 内部不响应 ctx.Done() 是优雅下线最大的隐形杀手
server.Shutdown() 不会杀掉 handler,它只等 handler 自己结束。如果 handler 卡在无 context 的 db.QueryRow()、http.DefaultClient.Do()、time.Sleep(10 * time.Second) 或死循环里,整个关闭流程就会卡死。
- 所有 handler 必须从
r.Context()或stream.Context()拿 context,并传递给下游阻塞调用 - 用
db.QueryRowContext(ctx, ...)替代db.QueryRow(...),用http.DefaultClient.Do(req.WithContext(ctx))替代裸调Do() - 长循环必须定期检测:
select { case ,不能只靠外部 <code>Shutdown()推动 - 中间件里启的 goroutine(如异步日志、指标上报)也必须监听同一
ctx,否则会拖慢 shutdown
真正难的不是写 shutdown 代码,而是让每一层都感知并响应同一个退出信号——从 listener、HTTP handler、gRPC stream、DB query 到 Kafka consumer,全部串联在一条 context 链上。漏掉任意一层,平滑下线就只是幻觉。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











