本篇为【写给 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.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()
是没有方法做判断的
业务错误码
那么如何做?在 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 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
呢?两者的性能极度重合
那有什么方法既能利用上 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 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
的互转
上文提到无论是 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 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)
引申问题
如何传递这个非标准的谬误扩大音讯呢?或者能够在下一章能够找到答案。
总结
咱们先介绍了 gRPC 最根本的错误处理形式:返回error
。
之后咱们又介绍了一种可能携带更多错误信息的形式:Status
,它蕴含 code
、message
、details
等信息,通过 Status
与error
的相互转换,利用 error
来传输谬误
参考
- gRPC 扩大错误处理
- google API 设计指南 - 谬误
✨ 微信公众号【凉凉的知识库】同步更新,欢送关注获取最新最有用的后端常识 ✨