grpc错误码必须用status.error构造,否则客户端收到codes.unknown;需导入grpc/status和grpc/codes包,错误消息应简洁且不包含敏感信息;结构化详情需用status.withdetails并注册类型。

gRPC 错误码必须用 status.Error 构造,不能直接返回 error 字符串
Go 的 gRPC 服务端如果只是 return errors.New("xxx") 或 fmt.Errorf("xxx"),客户端收到的永远是 codes.Unknown,且无法提取原始错误信息。gRPC 协议要求状态码、消息、详情(如 RetryInfo)必须通过 status.Status 编码进响应头。
正确做法是用 status.Error 显式构造:
import "google.golang.org/grpc/status"
func (s *Server) DoSomething(ctx context.Context, req *pb.Request) (*pb.Response, error) {
if req.Id == 0 {
return nil, status.Error(codes.InvalidArgument, "id cannot be zero")
}
return &pb.Response{}, nil
}
- 必须导入
google.golang.org/grpc/status和google.golang.org/grpc/codes -
status.Error第二个参数是 human-readable message,不是日志,别塞堆栈或敏感字段 - 直接
return errors.New(...)→ 客户端看到codes.Unknown,调试时一脸懵 - 用
status.Errorf可以格式化,但别滥用(比如拼接用户输入后塞进 message,有安全风险)
自定义错误详情要用 status.WithDetails,且类型必须注册
想传结构化错误信息(比如重试建议、业务错误码、字段名),得靠 status.WithDetails + 实现 protoc-gen-go 生成的 *errdetails.* 类型。但光塞进去没用——客户端反序列化失败,因为 gRPC 不知道这个类型。
服务端要注册该类型到 status 包:
立即学习“go语言免费学习笔记(深入)”;
import (
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc/status"
)
func (s *Server) DoSomething(ctx context.Context, req *pb.Request) (*pb.Response, error) {
if req.Timeout <= 0 {
st := status.New(codes.InvalidArgument, "timeout must be positive")
badReq := &errdetails.BadRequest{
FieldViolations: []*errdetails.BadRequest_FieldViolation{{
Field: "timeout",
Description: "must be greater than zero",
}},
}
// 必须调用 Register 一次(通常放 init 或 main)
st, _ = st.WithDetails(badReq)
return nil, st.Err()
}
return &pb.Response{}, nil
}
- 首次使用前需调用
errdetails.Register(推荐在main()开头执行一次) - 未注册就调用
WithDetails→ 客户端解析出空对象,st.Details()返回[]interface{}但里面全是nil - 细节类型必须是
protoc-gen-go生成的 pb struct,不能是自定义 Go struct - 一个
status.Status最多支持一种 detail 类型重复添加(后加的会覆盖前一个)
客户端解析错误要先用 status.FromError 解包,再判断 Code()
Go 客户端拿到的 error 是一个包装过的接口,直接用 errors.Is(err, xxx) 或 strings.Contains(err.Error(), "...") 都不可靠。真正可依赖的只有 status.Code() 和 status.Details()。
resp, err := client.DoSomething(ctx, req)
if err != nil {
st, ok := status.FromError(err)
if !ok {
// 不是 gRPC error,可能是网络断开、DNS 失败等底层错误
log.Printf("non-gRPC error: %v", err)
return
}
switch st.Code() {
case codes.InvalidArgument:
log.Printf("bad request: %s", st.Message())
// 解析 details
for _, detail := range st.Details() {
if br, ok := detail.(*errdetails.BadRequest); ok {
for _, vio := range br.FieldViolations {
log.Printf("field %s invalid: %s", vio.Field, vio.Description)
}
}
}
case codes.Unavailable:
// 可能需要重试
}
return
}
-
status.FromError是唯一安全解包方式;err.(interface{ Code() codes.Code })强转会 panic -
st.Message()是服务端传的 message,不是原始 error 的Error()方法结果 - 网络层错误(如连接拒绝)不会走
status,FromError返回ok=false,此时应按超时/重连逻辑处理 - 不要在
switch st.Code()里漏掉codes.OK—— 虽然正常情况不会进来,但防御性编程建议显式处理
HTTP/JSON 代理(如 grpc-gateway)会把 codes 映射成 HTTP 状态码,但映射表不完全对等
用 grpc-gateway 暴露 REST 接口时,codes.NotFound → 404,codes.InvalidArgument → 400,看起来很顺。但有几个坑容易踩:
-
codes.Unknown和codes.Internal都映射为500,前端无法区分是服务崩了还是协议错乱 -
codes.Unauthenticated→401,但若网关本身鉴权失败(比如 JWT 解析异常),它可能直接返回401而不经过你的服务逻辑 -
codes.PermissionDenied→403,但有些前端框架对403做了特殊拦截(比如自动跳登录页),而你本意只是“该用户无权操作此资源” - 自定义 detail 如果没被 gateway 显式支持(比如
errdetails.RetryInfo),会被丢弃,前端收不到重试建议
如果业务强依赖状态语义,建议在 response body 里冗余一份业务错误码(比如 error_code: "USER_NOT_FOUND"),别只靠 HTTP 状态码传递关键逻辑。










