grpc.UnaryServerInterceptor是统一错误处理的入口,因它能在所有一元RPC调用路径中拦截handler前后逻辑,天然适配status.Error序列化,确保错误标准化返回;必须配合errCodeMap映射业务错误到gRPC标准码,并单独处理context.DeadlineExceeded以避免语义失真。

为什么 grpc.UnaryServerInterceptor 是统一处理错误的入口
Go 的 gRPC 服务端错误分散在各 handler 内,直接 return err 会导致客户端收到原始错误(比如 status.Code = Unknown、堆栈泄漏),既不安全也不规范。拦截器是唯一能在所有 RPC 调用路径上“掐住脖子”的位置——它在 handler 执行前后都可介入,且天然接收 status.Error 类型的返回值,适合作为错误标准化出口。
注意:必须用 grpc.UnaryServerInterceptor(非 StreamServerInterceptor),因为绝大多数业务是 unary 场景;若混用 streaming,需额外补 streaming 拦截器,否则错误会漏掉。
- 拦截器函数签名固定:
func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) - 错误必须通过
status.Errorf或status.Error构造,gRPC 才能正确序列化为 wire-level 错误;裸errors.New会被转成Unknown码 - handler 执行后拿到的
err可能是 nil、*status.Status或普通 error,需统一转换,不能直接透传
如何把业务 error 映射成带 code 和 details 的标准 status.Status
核心是建立错误码映射表,并避免在 handler 里手动构造 status.Error。推荐在拦截器里统一做转换,让业务层只管抛出领域错误(如 ErrUserNotFound、ErrInvalidParam),由拦截器查表翻译。
示例映射结构:
立即学习“go语言免费学习笔记(深入)”;
var errCodeMap = map[error]codes.Code{
ErrUserNotFound: codes.NotFound,
ErrInvalidParam: codes.InvalidArgument,
ErrPermissionDenied: codes.PermissionDenied,
ErrInternal: codes.Internal,
}
拦截器内关键逻辑:
resp, err := handler(ctx, req)
if err != nil {
if s, ok := status.FromError(err); ok {
// 已是 status.Error,直接复用(如中间件提前 return status.Error)
return resp, err
}
// 普通 error → 查表转 code,再加通用 message
code := codes.Unknown
if mappedCode, ok := errCodeMap[err]; ok {
code = mappedCode
}
return resp, status.Errorf(code, "rpc error: %v", err.Error())
}
return resp, nil
- 不要用
fmt.Errorf("xxx: %w", err)包装后再传给status.Error,会导致原始 error type 丢失,查表失败 - 如果业务 error 实现了
GRPCStatus() *status.Status方法(如github.com/grpc-ecosystem/go-grpc-middleware/errors提供的类型),优先用该方法获取 code,比查表更灵活 - 细节(details)需显式附加:
status.WithDetails(...),否则前端拿不到结构化信息;常见场景如参数校验失败,应附带&errdetails.BadRequest{FieldViolations: [...]}
拦截器里加日志和监控时,为什么不能直接打 err.Error()
因为 err.Error() 对 *status.Status 返回的是包含 code 和 message 的字符串(如 "rpc error: code = NotFound desc = user not found"),但日志中真正需要的是可筛选的字段:code、method、request ID、耗时。硬解析字符串既脆弱又低效。
- 用
status.FromError(err)解包,再取s.Code()、s.Message(),安全可靠 - 务必记录
info.FullMethod(如"/user.UserService/GetUser"),这是定位问题接口的关键 - 敏感字段(如用户 token、手机号)必须脱敏——拦截器是最后一道防线,别让日志变成泄露源
- 监控指标建议按
code+FullMethod维度打点,避免只统计 total error count,掩盖具体失败分布
为什么 context.DeadlineExceeded 必须单独处理
超时错误不是业务逻辑错误,而是调用链路问题。若和其他 error 一样走映射表,会把 DeadlineExceeded 错标为 Internal 或 Unknown,导致告警失真、重试策略失效。
- 必须在拦截器开头或 handler 后立即检查:
if errors.Is(err, context.DeadlineExceeded),然后强制转成codes.DeadlineExceeded - 注意:
status.FromError(err).Code() == codes.DeadlineExceeded不可靠,因为某些中间件可能把 timeout 包装成其他 error type - 客户端侧要据此做退避重试(
codes.DeadlineExceeded可重试,codes.NotFound不该重试),服务端不区分就破坏了语义契约
真正麻烦的是跨服务调用链中的 timeout 传递——上游没设置 deadline、下游 panic 导致超时被吞、中间件未透传 cancel signal……这些不在拦截器能解决的范围,但至少拦截器得守住底线:不把 timeout 伪装成业务错。


















