gRPC 错误必须用 status.Error() 构造,原生 error 会被降级为 codes.Unknown;客户端须用 status.FromError() 解包并检查 Code();自定义详情需提前注册类型;codes.Unavailable 与 codes.Internal 语义不可混淆。

直接返回 errors.New() 或 fmt.Errorf(),客户端永远收到 codes.Unknown —— 这不是 bug,是 gRPC 协议强制要求:只有 status.Status 类型才能被序列化为带码、带消息、带详情的响应头。
服务端必须用 status.Error() 构造错误
gRPC 运行时只识别 status.Status 实例;任何原生 error 都会被降级为 codes.Unknown,且 grpc-status 响应头里填的是 12(即 Unknown 的整数值)。
- ✅ 正确写法:
return nil, status.Error(codes.NotFound, "user id 123 not found") - ❌ 错误写法:
return nil, errors.New("user not found")→ 客户端看到codes.Unknown,调试时无从下手 -
status.Errorf()可用于格式化,但别拼接用户输入(防信息泄露),也别塞堆栈(message 是给调用方看的,不是日志) - 如果用了中间件或拦截器,注意它是否调了
.Err()或status.Convert()—— 这些操作可能把status.Status又转回普通error,导致下游收不到真实码
客户端必须用 status.FromError() 解包
不能靠 strings.Contains(err.Error(), "not found") 或 errors.Is(err, xxx) 判断语义,因为原始 error 已被序列化/反序列化过,类型链已断。
- ✅ 正确流程:
st, ok := status.FromError(err),再判断st.Code() == codes.NotFound - ❌ 错误习惯:对
err.Error()做字符串匹配 —— 服务端一改提示语,客户端逻辑就失效 -
status.FromError()对非 gRPC 错误(如网络断开、DNS 失败)返回ok = false,此时应走兜底逻辑(比如重试前检查连接状态) - 若需提取结构化详情(如字段校验失败名),得配合
st.Details()+proto.Unmarshal(),但前提是服务端注册过该类型(见下一条)
自定义详情要用 status.WithDetails() 且提前注册类型
想传 errdetails.BadRequest 或自定义错误 proto message,光塞进去没用——客户端解包时会跳过未注册类型的 details 条目,返回空 slice。
立即学习“go语言免费学习笔记(深入)”;
- 服务端注册只需一次,推荐放在
main()开头:status.RegisterErrorDetail(&errdetails.BadRequest{}) - 构造时用
status.New().WithDetails(...).Err()或更简洁的status.WithDetails(status.Error(...), ...) - 客户端拿到
st.Details()后,每个 item 是proto.Message,必须用proto.Unmarshal()转成具体 struct,不能直接类型断言 - 别在
details里塞敏感数据(如密码、token),它会随响应头透出,网关或前端可能直接打印
codes.Unavailable 和 codes.Internal 别混用
这两个码决定客户端是否重试、告警是否触发、SLO 是否计入 —— 语义错位会导致故障响应策略完全跑偏。
-
codes.Unavailable:依赖临时不可达(DB 连接池满、下游 503、LB 未就绪),客户端可安全重试 -
codes.Internal:服务自身崩溃(panic 恢复后未处理、空指针、配置加载失败),不该重试,应立即告警+修复 - 常见陷阱:把数据库唯一约束冲突当成
Internal返回 —— 实际应映射为codes.AlreadyExists或codes.FailedPrecondition - context 超时/取消错误(
context.DeadlineExceeded,context.Canceled)不能包装成status.Error(),它们属于调用方控制流,不是服务端错误语义
最常被忽略的一点:错误码不是“写完就完”,它要贯穿日志、监控、重试、前端提示三层。一旦服务端用了 codes.Unknown,后续所有链路都失去语义能力 —— 不是代码没报错,而是错得悄无声息。


















