go服务必须显式调用signal.notify同时监听syscall.sigterm和os.interrupt(即sigint),使用make(chan os.signal, 1)带缓冲通道,且注册须在http.listenandserve等阻塞操作之前;关闭时应调用srv.shutdown()而非srv.close(),并传入带超时的context,再依次关闭db、grpc、kafka等下游资源。

Go 服务如何正确监听 SIGTERM 和 SIGINT
Go 程序默认不处理系统信号,必须显式调用 signal.Notify 才能捕获。只监听 SIGTERM 不够——本地开发常按 Ctrl+C(触发 SIGINT),而 Kubernetes 或 systemd 发送的是 SIGTERM。两者都得覆盖。
常见错误是把 signal.Notify 放在 goroutine 里却没做同步控制,导致主 goroutine 提前退出,信号还没来得及送达。
- 用
make(chan os.Signal, 1)创建带缓冲的 channel,避免信号丢失(尤其多信号快速到达时) - 始终同时监听
os.Interrupt(即SIGINT)和syscall.SIGTERM - 不要在
signal.Notify后立刻select,需确保 HTTP server 已真正启动再开始等信号
HTTP Server 关闭时为什么 srv.Shutdown 比 srv.Close 更安全
srv.Close() 是硬关闭:立即断开所有连接,正在处理的请求被中断,可能返回 500 或直接丢包;srv.Shutdown() 是软关闭:先禁止新连接,等待已有请求完成(或超时),再释放资源。
它依赖上下文控制超时,且必须在收到信号后立即调用——延迟调用等于放任请求被粗暴终止。
- 传入带超时的
context.WithTimeout,例如 10 秒:ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - 调用
srv.Shutdown(ctx)后必须cancel(),否则 ctx 泄漏 - 如果
Shutdown返回context.DeadlineExceeded,说明有请求卡住,此时可考虑记录日志但不应强行Close
哪些资源需要手动清理,又该怎么协调关闭顺序
HTTP Server 只是冰山一角。数据库连接池、消息队列消费者、定时任务、gRPC server、自定义后台 goroutine 都得在停机前收尾。顺序错了会出问题:比如先关 DB 连接池,但 HTTP handler 还在用它,就会 panic。
典型顺序是:停止接收新请求 → 等待活跃请求结束 → 关闭下游依赖(DB、MQ)→ 清理本地状态(临时文件、缓存)。
- 用
sync.WaitGroup管理后台 goroutine 的生命周期,每个 goroutine 在退出前调用wg.Done() - 数据库连接池关闭用
db.Close(),它本身会阻塞直到所有连接归还,但要确保此前无新查询发起 - 对长连接服务(如 WebSocket),需额外广播“即将下线”,并设置读写超时配合
Shutdown
测试优雅停机是否生效的三个关键检查点
本地跑一次 Ctrl+C 不代表线上可靠。真实场景中,Kubernetes 的 preStop hook、容器 runtime 的信号转发、进程管理器(如 supervisord)的配置都会影响信号送达时机与语义。
最容易被忽略的是:信号是否真的发到了 Go 主进程?有些容器镜像用 shell 启动(如 sh -c "./app"),这时 SIGTERM 会发给 shell 而非 Go 进程,导致完全收不到。
- 在容器中运行
ps aux,确认 Go 二进制是 PID 1;如果不是,改用exec ./app启动 - 加一行日志到信号处理函数开头,用
log.Printf("got signal: %v", sig),验证是否真被触发 - 用
curl -X POST http://localhost:8080/health持续轮询,在发kill -TERM $(pidof app)后观察是否还能响应几秒(取决于 Shutdown 超时)
停机逻辑越靠后越难调试——信号接收、上下文取消、goroutine 协作、资源释放,任何一环没对齐,就变成“以为优雅,实则中断”。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











