应启独立goroutine监听信号:sigchan := make(chan os.signal, 1); signal.notify(sigchan, syscall.sigint, syscall.sigterm); go func() { for range sigchan { os.exit(0) } }()

用 os.Signal 捕获退出信号但别阻塞主 goroutine
Go 进程无法靠 defer 捕获 SIGKILL,但 SIGINT、SIGTERM 必须响应——否则 Kubernetes 的 preStop 或 kill -15 会直接超时强杀。常见错误是把 signal.Notify 放在主线程里用 for range 同步阻塞读,导致其他初始化逻辑卡住。
正确做法是起一个独立 goroutine 监听,并用 channel 通知主流程:
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
<p>注意:<code>os.Signal</code> channel 容量必须 ≥1,否则第一个信号可能丢失;不要在 signal handler 里做耗时操作(如写文件、发 HTTP),应转交 shutdown 流程统一处理。</p><h3>用 <code>sync.WaitGroup</code> + <code>context.WithTimeout</code> 控制优雅退出时限</h3><p>服务关闭时,HTTP server、gRPC server、数据库连接池、后台定时任务都要停,但不能无限等。典型陷阱是只调 <code>srv.Shutdown()</code> 却没设超时,或对每个组件单独设 timeout 导致总耗时不可控。</p><p>推荐结构是:所有可关闭资源注册到同一个 <code>sync.WaitGroup</code>,主 shutdown 流程用 <code>context.WithTimeout</code> 统一兜底:</p>
- 每个资源的
Close()或Shutdown()方法调用前wg.Add(1) - goroutine 内执行关闭逻辑,完成后
wg.Done() - 主流程调
wg.Wait()前启动 context 超时等待,超时后直接返回(此时部分资源可能未关干净,但进程必须退出)
例如 gRPC server 关闭:
go func() {
defer wg.Done()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := grpcServer.GracefulStop(); err != nil {
log.Warn("grpc graceful stop failed", "err", err)
}
}()
panic 不能靠 recover 全局捕获,要分层隔离
在 main() 函数里用 defer/recover 只能捕获 main goroutine 的 panic,HTTP handler、定时任务、消息消费 goroutine 的 panic 会直接终止进程。更糟的是,有些 panic(如空指针解引用、切片越界)本就不该 recover,掩盖问题反而危险。
真正可行的策略是分层防御:
- HTTP handler 层:用中间件包裹每个 handler,
recover()并记录 stack trace 到日志,返回 500,不 panic 主流程 - 后台 goroutine 层:所有
go func() { ... }()外层加defer func(){ if r := recover(); r != nil { log.Error("goroutine panic", "panic", r) } }() - 关键路径(如 DB 写入、支付回调)仍保留显式
if err != nil判断,不依赖 panic 作控制流
注意:recover() 只在 defer 函数中有效,且只能捕获当前 goroutine 的 panic。
异常监控上报要带上下文,避免 panic 信息孤岛
只打印 log.Fatal(err) 或 fmt.Printf("%+v", err) 会导致线上问题难定位:不知道发生在哪个请求、哪个用户、哪个配置版本。很多团队用 zap 或 zerolog 但没传 request ID 和 trace ID。
建议在全局 logger 初始化时注入静态字段(如 service name、host、pid),再在每个业务入口(HTTP middleware、消息 handler)动态注入请求级字段:
logger = logger.With().Str("req_id", reqID).Str("trace_id", traceID).Logger()
panic 上报时,除了 error 和 stack,至少附加:
-
runtime.NumGoroutine()—— 判断是否 goroutine 泄漏 -
debug.ReadGCStats()—— 看 GC 是否频繁触发 - 当前 goroutine 的
runtime.Stack()(非 panic goroutine 的栈也值得看)
这些数据不一定要实时发远端,先写本地 ring buffer 日志,由 agent 异步采集更可靠。
进程生命周期最难的不是“怎么启”或“怎么停”,而是“停到一半又收到新请求”“panic 发生在 signal handler 里”“shutdown 时 DB 连接池已关闭但还有 goroutine 在用它”——这些边界场景得靠组合 context、WaitGroup、channel 和分层 recover 来覆盖,少一个环节就可能静默失败。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











