http handler必须显式defer-recover,因为http.servemux在新goroutine中调用业务函数,而recover仅对同goroutine有效;默认defaultservemux无recover机制,handler内panic会导致进程退出、pod持续重启。

Go 微服务里 panic 不该跨 goroutine 传播,HTTP handler 和 gRPC 方法必须显式加 defer recover(),否则一次 nil pointer 就能让整个服务进程退出,K8s 持续重启 Pod。
为什么 HTTP handler 必须自己包 defer-recover
Go 的 http.ServeMux 和 grpc.Server 都会在新 goroutine 中调用你的业务函数,而 recover() 只对同 goroutine 有效。默认的 http.DefaultServeMux 并不自带 recover,所以你写的 handler 一旦 panic,进程直接终止。
- 常见错误现象:
panic: runtime error: invalid memory address or nil pointer dereference导致服务整机挂掉 - 正确做法:每个 handler 函数开头写
defer func() { if r := recover(); r != nil { /* 记录日志 + 返回 500 */ } }() - 更推荐方式:统一注册中间件(如 Gin 的
RecoveryMiddleware或 net/http 的自定义http.Handler包装器) - 注意:不要在 recover 里再 panic,否则等于没处理
gRPC 服务必须用 status.Error 而非 errors.New
gRPC 客户端靠 status.FromError() 解析错误码,如果服务端返回的是裸 errors.New("xxx") 或 fmt.Errorf("xxx"),客户端拿到的永远是 codes.Unknown,无法做重试或降级判断。
- 正确返回:
status.Error(codes.Internal, "db connection failed")或status.Errorf(codes.Unavailable, "upstream timeout") - 业务错误建议用
codes.InvalidArgument、codes.NotFound、codes.ResourceExhausted等语义化码 - 避免在 message 字段拼接敏感信息(如用户 ID、token),应单独结构化打点到日志字段
- 第三方库如
github.com/segmentio/errors可辅助封装带 context 的 error,但最终仍需转成status.Error
error 包装要用 %w,别用 %v 或 +
只有用 %w 才能保留原始错误链,让上层通过 errors.Is() 或 errors.As() 做类型判断和精准恢复;用 + "failed" 或 sprintf("%v", err) 会切断错误链,丢失底层原因。
- ✅ 正确:
return fmt.Errorf("fetch user failed: %w", err) - ❌ 错误:
return errors.New("fetch user failed: " + err.Error()) - ❌ 错误:
return fmt.Errorf("fetch user failed: %v", err) - 配合
errors.Is(err, context.DeadlineExceeded)可区分超时与真实失败,决定是否重试 - 若下游 SDK 没透传 context(如老版 mongo-go-driver),超时后 goroutine 仍在跑,错误包装再好也救不了连接泄漏
recover 自身不能 panic,且必须在 defer 中调用
recover() 只有在 defer 函数中被直接调用才有效;如果包了一层函数再调,就捕获不到。而且 recover 处理逻辑本身如果 panic,会导致 defer 链断裂,错误彻底丢失。
- ❌ 无效写法:
defer func() { handlePanic(recover()) }()——recover()不在 defer 直接作用域 - ✅ 有效写法:
defer func() { if r := recover(); r != nil { log.Printf("panic: %v", r) } }() - log 时建议用
debug.PrintStack()补充堆栈,但生产环境慎用(性能开销大) - recover 后不应继续执行业务逻辑,应立即
c.Abort()(Gin)或w.WriteHeader(500)(net/http)并返回
最常被忽略的一点:错误处理逻辑自身不可 panic —— 这不是建议,是硬约束。哪怕只是日志打点、JSON 序列化响应体,都可能因空指针或并发写 panic,导致 recover 失效。所有 recover 块内的代码,都要当作“最后防线”来写防御性逻辑。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











