本篇为【写给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
来传输错误
参考
✨ 微信公众号【凉凉的知识库】同步更新,欢迎关注获取最新最有用的后端知识 ✨