本篇为【写给go开发者的gRPC教程】系列第四篇
第一篇:protobuf根底
第二篇:通信模式
第三篇:拦截器
第四篇:错误处理
本系列将继续更新,欢送关注获取实时告诉
根本错误处理
首先回顾下pb文件和生成进去的client与server端的接口
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的值判断么?
// common/errors.govar ParamsErr = errors.New("params is not valid")
// server/main.gofunc (s *OrderManagementImpl) GetOrder(ctx context.Context, orderId *wrapperspb.StringValue) (*pb.Order, error) { return nil, common.ParamsErr}
// client/main.goretrievedOrder, 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()
是没有方法做判断的
业务错误码
那么如何做?在http的服务中,咱们会应用错误码的形式来辨别不同谬误,通过判断errno
来辨别不同谬误
{ "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 Orderresp, 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
呢?两者的性能极度重合
那有什么方法既能利用上error
这个返回值,又能让client
端枚举出不同谬误么?一个十分直观的想法:让error
里记录枚举值就能够了!
但咱们都晓得Go里的error
是只有一个string
的,能够携带的信息相当无限,如何传递足够多的信息呢?gRPC
官网提供了google.golang.org/grpc/status
的解决方案
应用 Status
处理错误
gRPC
提供了google.golang.org/grpc/status
来示意谬误,这个构造蕴含了 code
和 message
两个字段
code
是相似于http status code
的一系列谬误类型的枚举,所有语言 sdk 都会内置这个枚举列表
尽管总共预约义了16个code
,但gRPC
框架并不是用到了每一个code,有些code仅提供给业务逻辑应用
Code | Number | Description |
---|---|---|
OK | 0 | 胜利 |
CANCELLED | 1 | 调用勾销 |
UNKNOWN | 2 | 未知谬误 |
... | ... | ... |
message
就是服务端须要告知客户端的一些谬误详情信息
package mainimport ( "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
的互转
上文提到无论是server
和client
返回的都是error
,如果咱们返回Status
那必定是不行的
但 Status
提供了和Error
互转的办法
所以在服务端能够利用.Err()
把Status
转换成error
并返回
或者间接创立一个Status
的error
:status.Errorf(codes.InvalidArgument, "invalid args")
返回
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
的个数也是无限的。有个很大的问题就是 表达能力十分无限
所以咱们须要一个可能额定传递业务错误信息字段的性能
Richer error model
Google 基于本身业务, 有了一套谬误扩大 https://cloud.google.com/apis...
// 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 类型, 反对咱们自行扩大
应用示例
因为 Golang 反对了这个扩大, 所以能够看到 Status
间接就是有 details
字段的.
所以应用 WithDetails
附加本人扩大的谬误类型, 该办法会主动将咱们的扩大类型转换为 Any 类型
WithDetails
返回一个新的 Status
其蕴含咱们提供的details
WithDetails
如果遇到谬误会返回nil
和第一个谬误
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 谬误的最常见需要,例如配额失败和有效参数。与错误代码一样,开发者应尽可能应用这些规范载荷
上面是一些示例 error_details
载荷:
ErrorInfo
提供既稳固又可扩大的结构化错误信息。RetryInfo
:形容客户端何时能够重试失败的申请,这些内容可能在以下办法中返回:Code.UNAVAILABLE
或Code.ABORTED
QuotaFailure
:形容配额查看失败的形式,这些内容可能在以下办法中返回:Code.RESOURCE_EXHAUSTED
BadRequest
:形容客户端申请中的违规行为,这些内容可能在以下办法中返回:Code.INVALID_ARGUMENT
服务端
package mainimport ( "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 Orderorder, 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)
引申问题
如何传递这个非标准的谬误扩大音讯呢?或者能够在下一章能够找到答案。
总结
咱们先介绍了gRPC最根本的错误处理形式:返回error
。
之后咱们又介绍了一种可能携带更多错误信息的形式:Status
,它蕴含code
、message
、details
等信息,通过Status
与error
的相互转换,利用error
来传输谬误
参考
- gRPC 扩大错误处理
- google API 设计指南-谬误
✨ 微信公众号【凉凉的知识库】同步更新,欢送关注获取最新最有用的后端常识 ✨