当前位置: 首页 > news >正文

Go语言的gRPC教程-错误处理

一、基本错误处理

pb文件和生成出来的client与server端的接口

service SearchService {rpc Search(SearchRequest) returns (SearchResponse) {}
}
type SearchServiceServer interface {Search(context.Context, *SearchRequest) (*SearchResponse, error)mustEmbedUnimplementedSearchServiceServer()
}
type SearchServiceClient interface {Search(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (*SearchResponse, error)
}

可以看到,虽然我们没有在pb文件中的接口定义设置error返回值,但生成出来的go代码是包含error返回值的

这非常符合Go语言的使用习惯:通常情况下我们定义多个error变量,并且在函数内返回,调用方可以使用errors.Is()或者errors.As()对函数的error进行判断

var (ParamsErr = errors.New("params err")InernalErr    = errors.New("internal err")
)func Invoke(i bool) error {if i {return ParamsErr} else {return InernalErr}
}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("internal error")}}
}

🌿 但在RPC场景下,我们还能进行error的值判断吗?

var ParamsErr = errors.New("params is not valid")
// server/main.go
func (s *HelloWorldServer) Search(ctx context.Context, r *helloworld.SearchRequest) (*helloworld.SearchResponse, error) {return nil, ParamsErr
}
resp, err := client.Search(context.Background(), &helloworld.SearchRequest{Request:  "Golang",Keywords: []string{"hello", "world"},})if err != nil && errors.Is(err, ParamsErr) {// 不会走到这里,因为err和ParamsErr不相等panic(err)
}

很明显,serverclient并不在同一个进程甚至都不在同一个台机器上,所以errors.Is()或者errors.As()是没有办法做判断的

1.1 业务错误码

在http的服务中,我们会使用错误码的方式来区分不同错误,通过判断code来区分不同错误

{"code": 0,"msg": "ok","data": {}
}{"code": 1000,"msg": "params error","data": {}
}

类似的,我们调整下我们pb定义:在返回值里携带错误信息

service SearchService {rpc Search(SearchRequest) returns (SearchResponse) {}
}message SearchRequest {string request = 1;repeated string keywords = 2;Book book = 3;
}enum Status {Ok = 0;ParamsErr = 1;ServerErr = 2;
}message SearchResponse {Status status = 1;string response = 2;
}

于是在服务端实现的时候,我们可以返回对应数据或者错误状态码

func (server *HelloWorldServer) Search(ctx context.Context, r *helloworld.SearchRequest) (*helloworld.SearchResponse, error) {fmt.Println("Request received:", r.GetRequest())fmt.Println("Keywords received:", r.GetKeywords())fmt.Println("Books received:", r.GetBook())if r.GetRequest() == "" {return &helloworld.SearchResponse{Status:  helloworld.Status_ParamsErr,Message: "request is empty",}, nil}return &helloworld.SearchResponse{Status:   helloworld.Status_Ok,Message:  "success",Response: fmt.Sprintf("hello %s", r.GetRequest()),}, nil
}

在客户端可以判断返回值的错误码来区分错误,这是我们在常规RPC的常见做法

resp, err := client.Search(context.Background(), &helloworld.SearchRequest{Request:  "Golang",Keywords: []string{"hello", "world"},Book: &helloworld.Book{Title: "Go语言编程",Price: &wrapperspb.Int64Value{Value: 100,},},})if err != nil {panic(err)}if resp.GetStatus() != helloworld.Status_Ok {fmt.Println(resp.GetMessage())} else {fmt.Println(resp.GetResponse())}

🌿 但,这么做有什么问题么?

很明显,对于clinet侧来说,本身就可能遇到网络失败等错误,所以返回值(*helloworld.SearchResponse, error)包含error并不会非常突兀

但再看一眼server侧的实现,我们把错误枚举放在SearchResponse中,此时返回的另一个error就变得非常尴尬了,该继续返回一个error呢,还是直接都返回nil呢?两者的功能极度重合

那有什么办法既能利用上error这个返回值,又能让client端枚举出不同错误么?一个非常直观的想法:让error里记录枚举值就可以了!

但我们都知道Go里的error是只有一个string的,可以携带的信息相当有限,如何传递足够多的信息呢?gRPC官方提供了google.golang.org/grpc/status的解决方案

二、使用 Status处理错误

gRPC 提供了google.golang.org/grpc/status来表示错误,这个结构包含了 codemessage 两个字段

🌲 code是类似于http status code的一系列错误类型的枚举,所有语言 sdk 都会内置这个枚举列表

虽然总共预定义了16个code,但gRPC框架并不是用到了每一个code,有些code仅提供给业务逻辑使用

CodeNumberDescription
OK0成功
CANCELLED1调用取消
UNKNOWN2未知错误

🌲 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)
}

2.1 Status 和语言 Error 的互转

上文提到无论是serverclient返回的都是error,如果我们返回Status那肯定是不行的

Status 提供了和Error互转的方法

在这里插入图片描述

所以在服务端可以利用.Err()Status转换成error并返回

或者直接创建一个Statuserrorstatus.Errorf(codes.InvalidArgument, "invalid args")返回

func (server *HelloWorldServer) Search(ctx context.Context, r *helloworld.SearchRequest) (*helloworld.SearchResponse, error) {if r.GetRequest() == "" {return nil, status.Error(codes.InvalidArgument, "Request is empty")}return &helloworld.SearchResponse{Response: fmt.Sprintf("hello %s", r.GetRequest()),}, status.New(codes.OK, "success").Err()
}

到客户端这里我们再利用status.FromError(err)error转回Status

resp, err := client.Search(context.Background(), &helloworld.SearchRequest{Request:  "Golang",Keywords: []string{"hello", "world"},})if err != nil {// 转换有可能失败stat, ok := status.FromError(err)if ok && stat.Code() == codes.InvalidArgument {fmt.Println(stat.Code(), stat.Message())} else {fmt.Println(err)panic(err)}return}

🌿 但,status真的够用么?

类似于HTTP 状态码code的个数也是有限的。有个很大的问题就是 表达能力非常有限

所以我们需要一个能够额外传递业务错误信息字段的功能

三、Richer error model

Google 基于自身业务, 有了一套错误扩展 https://cloud.google.com/apis/design/errors#error_model

// 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 类型, 支持我们自行扩展

3.1 使用示例

由于 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(&helloworld.SearchResponse{Response: "hello world",}); err == nil {return details.Err()}return st.Err()
}

前面提到details 数组字段, 而且这个字段是 Any 类型, 支持我们自行扩展。

同时,Google API 为错误详细信息定义了一组标准错误负载,您可在 google/rpc/error_details.proto 中找到这些错误负载

它们涵盖了对于 API 错误的最常见需求,例如配额失败和无效参数。与错误代码一样,开发者应尽可能使用这些标准载荷

下面是一些示例 error_details 载荷:

  • ErrorInfo 提供既稳定可扩展的结构化错误信息。
  • RetryInfo:描述客户端何时可以重试失败的请求,这些内容可能在以下方法中返回:Code.UNAVAILABLECode.ABORTED
  • QuotaFailure:描述配额检查失败的方式,这些内容可能在以下方法中返回:Code.RESOURCE_EXHAUSTED
  • BadRequest:描述客户端请求中的违规行为,这些内容可能在以下方法中返回:Code.INVALID_ARGUMENT
服务端
package mainimport ("fmt"helloworld "example.com/grpc/helloworld_proto""google.golang.org/genproto/googleapis/rpc/errdetails""google.golang.org/grpc/codes""google.golang.org/grpc/status"
)func (server *HelloWorldServer) Search(ctx context.Context, r *helloworld.SearchRequest) (*helloworld.SearchResponse, error) {if r.GetRequest() == "" {st := status.New(codes.InvalidArgument, "request is empty")details, err := st.WithDetails(&errdetails.BadRequest_FieldViolation{Field:       "request",Description: "request is empty",})if err == nil {return nil, details.Err()}return nil, st.Err()}return &helloworld.SearchResponse{Response: fmt.Sprintf("hello %s", r.GetRequest()),}, status.New(codes.OK, "success").Err()
}
客户端
resp, err := client.Search(context.Background(), &helloworld.SearchRequest{Request:  "Golang",Keywords: []string{"hello", "world"},Book: &helloworld.Book{Title: "Go语言编程",Price: &wrapperspb.Int64Value{Value: 100,},},})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 *errdetails.BadRequest_FieldViolation:fmt.Printf("Request Field Invalid: %s", info)default:log.Printf("Unexpected error type: %s", info)}}default:log.Printf("Unhandled error : %s ", st.String())}return}

3.2 问题

如何传递这个非标准的错误扩展消息呢?

四、总结

我们先介绍了gRPC最基本的错误处理方式:返回error

之后我们又介绍了一种能够携带更多错误信息的方式:Status,它包含codemessagedetails等信息,通过Statuserror的互相转换,利用error来传输错误

参考

  • gRPC 扩展错误处理

  • google API 设计指南-错误

  • gRPC教程-错误处理

http://www.dtcms.com/a/316185.html

相关文章:

  • Android AppSearch 深度解析:现代应用搜索架构与实践
  • Elasticsearch向量库
  • 【web应用】前后端分离项目基本框架组成:Vue + Spring Boot 最佳实践指南
  • 深度解析 TCP 三次握手与四次挥手:从原理到 HTTP/HTTPS 的应用
  • 微服务—OpenFeign
  • Spring中七种Propagation类的事务属性详解
  • 研发团队看板协作中的自动化实践:集成CI/CD与任务流转
  • 007TG洞察:高效运营Telegram私域流量:技术挑战与自动化解决方案探索
  • 中科米堆CASAIM自动化三维扫描系统自动测量压铸件尺寸
  • 【原创】基于gemini-2.5-flash-preview-05-20多模态模型实现短视频的自动化二创
  • 从 “看懂图” 到 “读懂视频”:多模态技术如何用文本反哺视觉?
  • 原型模式在C++中的实现与面向对象设计原则
  • 二维数点问题 1
  • 学习日志28 python
  • AI编程新时代:从氛围编程到上下文编程的深度实践和思考
  • 鸿蒙开发、大数据开发、Java开发与前端开发全面对比解析
  • 【银行测试】银行票据项目业务+票据测试点分析(四)
  • 2025《艾诺提亚失落之歌》逆向工程解包尝试
  • Linux网络编程:TCP初体验
  • VirtualBox安装教程
  • 64位程序调用32位dll方法
  • 【Linux系统编程】线程概念与控制
  • 使用valgrind工具检测server端lib库的内存泄漏
  • FT5X06 触摸芯片
  • 【技术教程】如何将 ONLYOFFICE 文档连接到 Confluence
  • nodejs 编程基础01-NPM包管理
  • Android 之 Kotlin
  • 让 Spark 干体力活:用 Java 快速找出最小值
  • GaussDB 并行创建索引
  • Webpack 5 Module Federation 模块共享最佳实践