service OrderManagement { rpc getOrder(google.protobuf.StringValue) returns (Order); } type OrderManagementClient interface { GetOrder(ctx context.Context, in *wrapperspb.StringValue, opts ...grpc.CallOption) (*Order, error) } type OrderManagementServer interface { GetOrder(context.Context, *wrapperspb.StringValue) (*Order, error) mustEmbedUnimplementedOrderManagementServer() }可以看到,虽然我们没有在pb文件中的接口定义设置error返回值,但生成出来的go代码是包含error返回值的。这非常符合Go语言的使用习惯:通常情况下我们定义多个error变量,并且在函数内返回,调用方可以使用errors.Is()或者errors.As()对函数的error进行判断
var ( ParamsErr = errors.New("params err") BizErr = errors.New("biz err") ) func Invoke(i bool) error { if i { return ParamsErr } else { return BizErr } } func main() { err := Invoke(true) if err != nil { switch { case errors.Is(err, ParamsErr): log.Println("params error") case errors.Is(err, BizErr): log.Println("biz error") } } }但,在RPC场景下,我们还能进行error的值判断么?
// 堆代码 duidaima.com // common/errors.go var ParamsErr = errors.New("params is not valid") // server/main.go func (s *OrderManagementImpl) GetOrder(ctx context.Context, orderId *wrapperspb.StringValue) (*pb.Order, error) { return nil, common.ParamsErr } // client/main.go retrievedOrder, err := client.GetOrder(ctx, &wrapperspb.StringValue{Value: "101"}) if err != nil && errors.Is(err, common.ParamsErr) { // 不会走到这里,因为err和common.ParamsErr不相等 panic(err) }很明显,server和client并不在同一个进程甚至都不在同一个台机器上,所以errors.Is()或者errors.As()是没有办法做判断的
{ "errno": 0, "msg": "ok", "data": {} } { "errno": 1000, "msg": "params error", "data": {} }类似的,我们调整下我们pb定义:在返回值里携带错误信息
service OrderManagement { rpc getOrder(google.protobuf.StringValue) returns (GetOrderResp); } message GetOrderResp{ BizErrno errno = 1; string msg = 2; Order data = 3; } enum BizErrno { Ok = 0; ParamsErr = 1; BizErr = 2; } message Order { string id = 1; repeated string items = 2; string description = 3; float price = 4; string destination = 5; }于是在服务端实现的时候,我们可以返回对应数据或者错误状态码
func (s *OrderManagementImpl) GetOrder(ctx context.Context, orderId *wrapperspb.StringValue) (*pb.GetOrderResp, error) { ord, exists := orders[orderId.Value] if exists { return &pb.GetOrderResp{ Errno: pb.BizErrno_Ok, Msg: "Ok", Data: &ord, }, nil } return &pb.GetOrderResp{ Errno: pb.BizErrno_ParamsErr, Msg: "Order does not exist", }, nil }在客户端可以判断返回值的错误码来区分错误,这是我们在常规RPC的常见做法
// Get Order resp, err := client.GetOrder(ctx, &wrapperspb.StringValue{Value: ""}) if err != nil { panic(err) } if resp.Errno != pb.BizErrno_Ok { panic(resp.Msg) } log.Print("GetOrder Response -> : ", resp.Data)但,这么做有什么问题么?很明显,对于clinet侧来说,本身就可能遇到网络失败等错误,所以返回值(*GetOrderResp, error)包含error并不会非常突兀。但再看一眼server侧的实现,我们把错误枚举放在GetOrderResp中,此时返回的另一个error就变得非常尴尬了,该继续返回一个error呢,还是直接都返回nil呢?两者的功能极度重合!
Code Number Description OK 0 成功 CANCELLED 1 调用取消 UNKNOWN 2 未知错误 ... ... ...message就是服务端需要告知客户端的一些错误详情信息
package main import ( "errors" "fmt" "log" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) func Invoke() { ok := status.New(codes.OK, "ok") fmt.Println(ok) invalidArgument := status.New(codes.InvalidArgument, "invalid args") fmt.Println(invalidArgument) }Status 和语言 Error 的互转
func (s *OrderManagementImpl) GetOrder(ctx context.Context, orderId *wrapperspb.StringValue) (*pb.Order, error) { ord, exists := orders[orderId.Value] if exists { return &ord, status.New(codes.OK, "ok").Err() } return nil, status.New(codes.InvalidArgument, "Order does not exist. order id: "+orderId.Value).Err() }到客户端这里我们再利用status.FromError(err)把error转回Status
order, err := client.GetOrder(ctx, &wrapperspb.StringValue{Value: ""}) if err != nil { // 转换有可能失败 st, ok := status.FromError(err) if ok && st.Code() == codes.InvalidArgument { log.Println(st.Code(), st.Message()) } else { log.Println(err) } return } log.Print("GetOrder Response -> : ", order)但,status真的够用么?类似于HTTP 状态码code的个数也是有限的。有个很大的问题就是 表达能力非常有限,所以我们需要一个能够额外传递业务错误信息字段的功能。
// The `Status` type defines a logical error model that is suitable for // different programming environments, including REST APIs and RPC APIs. message Status { // A simple error code that can be easily handled by the client. The // actual error code is defined by `google.rpc.Code`. int32 code = 1; // A developer-facing human-readable error message in English. It should // both explain the error and offer an actionable resolution to it. string message = 2; // Additional error information that the client code can use to handle // the error, such as retry info or a help link. repeated google.protobuf.Any details = 3; }可以看到比标准错误多了一个 details 数组字段, 而且这个字段是 Any 类型, 支持我们自行扩展。
func InvokRPC() error { st := status.New(codes.InvalidArgument, "invalid args") if details, err := st.WithDetails(&pb.BizError{}); err == nil { return details.Err() } return st.Err() }前面提到details 数组字段, 而且这个字段是 Any 类型, 支持我们自行扩展。同时,Google API 为错误详细信息定义了一组标准错误负载,您可在 google/rpc/error_details.proto 中找到这些错误负载。它们涵盖了对于 API 错误的最常见需求,例如配额失败和无效参数。与错误代码一样,开发者应尽可能使用这些标准载荷。
BadRequest:描述客户端请求中的违规行为,这些内容可能在以下方法中返回:Code.INVALID_ARGUMENT
package main import ( "fmt" pb "github.com/liangwt/note/grpc/error_handling/error" epb "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) func (s *OrderManagementImpl) GetOrder(ctx context.Context, orderId *wrapperspb.StringValue) (*pb.Order, error) { ord, exists := orders[orderId.Value] if exists { return &ord, status.New(codes.OK, "ok").Err() } st := status.New(codes.InvalidArgument, "Order does not exist. order id: "+orderId.Value) details, err := st.WithDetails( &epb.BadRequest_FieldViolation{ Field: "ID", Description: fmt.Sprintf("Order ID received is not valid"), }, ) if err == nil { return nil, details.Err() } return nil, st.Err() }客户端
// Get Order order, err := client.GetOrder(ctx, &wrapperspb.StringValue{Value: ""}) if err != nil { st, ok := status.FromError(err) if !ok { log.Println(err) return } switch st.Code() { case codes.InvalidArgument: for _, d := range st.Details() { switch info := d.(type) { case *epb.BadRequest_FieldViolation: log.Printf("Request Field Invalid: %s", info) default: log.Printf("Unexpected error type: %s", info) } } default: log.Printf("Unhandled error : %s ", st.String()) } return } log.Print("GetOrder Response -> : ", order)引申问题