gRPC code
当发起 gRPC 调用时,客户端会接收成功状态的响应或带有对应错误状态的错误;在编写客户端应用程序时需处理所以潜在的错误和错误条件,编写服务端应用程序也需要处理错误并生成适当的错误状态码。
当发生错误时,gRPC 会返回一个错误状态码并附带一条可选的错误消息,该消息提供错误条件的更多细节。类似于 HTTP 定义了一套响应状态码,gRPC 也定义有一些状态码,Go 语言中此状态码由 codes 定义,本质上是一个 uint32 类型,使用时需导入 google.golang.org/grpc/codes
包。
type Code uint32
import "google.golang.org/grpc/codes"
gRPC 使用一组定义好的专用状态码,目前已经定义的状态码有如下几种:
Code | 值 | 含义 |
---|---|---|
OK | 0 | 请求成功,成功状态 |
Canceled | 1 | 操作已被(被调用)取消 |
Unknown | 2 | 未知错误,如果从另一个地址空间接收到的状态值属于在该地址空间中未知的错误空间,则可以返回此错误的示例,没有返回足够的错误信息的 API 引发的错误也可能会转换为此错误 |
InvalidArgument | 3 | 表示客户端指定的参数无效(注:这与 FailedPrecondition 不同),它表示无论系统状态如何都有问题的参数(如格式错误的文件名) |
DeadlineExceeded | 4 | 表示操作在完成之前已过期,超过截止时间,对于改变系统状态的操作,即使操作成功完成,也可能会返回此错误(如来自服务器的成功响应可能已延迟足够长的时间以使截止日期到期) |
NotFound | 5 | 表示未找到某些请求的实体(如文件或目录) |
AlreadyExists | 6 | 创建实体的尝试失败,因为实体已经存在 |
PermissionDenied | 7 | 表示调用者没有权限执行指定的操作, 它不能用于拒绝由耗尽某些资源引起的(使用 ResourceExhausted ), 如果无法识别调用者,也不能使用它(使用 Unauthenticated ) |
ResourceExhausted | 8 | 表示某些资源已耗尽,可能是每个用户的配额,或者整个文件系统空间不足 |
FailedPrecondition | 9 | 指示操作被拒绝,因为系统未处于操作执行所需的状态(如要删除的目录可能是非空的,rmdir 操作应用于非目录等) |
Aborted | 10 | 表示操作被中止,通常是由于并发问题,如排序器检查失败、事务中止等 |
OutOfRange | 11 | 表示尝试超出有效范围的操作 |
Unimplemented | 12 | 表示此服务中未实施或不支持/启用操作 |
Internal | 13 | 意味着底层系统预期的一些不变量已被破坏,若看到这个错误,则说明问题很严重 |
Unavailable | 14 | 表示服务当前不可用,这很可能是暂时的情况,可以通过回退重试来纠正(请注意:重试非幂等操作并不总是安全的) |
DataLoss | 15 | 表示不可恢复的数据丢失或损坏 |
Unauthenticated | 16 | 表示请求没有用于操作的有效身份验证凭据 |
_maxCode | 17 | - |
gRPC Status
RPC 服务的方法应该返回 nil 或来自 status.Status 类型的错误,客户端可以直接访问错误,Go 语言使用 gRPC Status 时需导入 google.golang.org/grpc/status
包。
import "google.golang.org/grpc/status"
当遇到错误时,gRPC 服务的方法函数应该创建一个 status.Status
,通常调用 status.New() 函数并传入适当的 status.Code
和错误描述来生成一个 status.Status
,调用 status.Err() 方法便能将一个 status.Status
转为 error 类型或者调用 status.Error() 方法直接生成 error ,下面是两种方式的比较:
// 创建 status.Status
st := status.New(codes.NotFound, "some description")
err := st.Err() // 转为error类型
// vs
err := status.Error(codes.NotFound, "some description")
在某些情况下,可能需要为服务器端的特定错误添加详细信息 ,status.WithDetails() 方法可以添加任意多个 proto.Message
,可以使用 google.golang.org/genproto/googleapis/rpc/errdetails
包中的定义或自定义的错误详情,例如以下的代码形式:
st := status.New(codes.ResourceExhausted, "Request limit exceeded.")
ds, _ := st.WithDetails(
// proto.Message
)
return nil, ds.Err()
客户端可以通过首先将普通 error 类型转换回 status.Status
,然后调用 status.Details() 方法来读取这些详细信息,例如以下的代码形式:
s := status.Convert(err)
for _, d := range s.Details() {
// ...
}
gRPC 程序示例
假如为 hello 服务设置访问限制,每个 name 只能调用一次 SayHello() 方法,超过此限制就返回一个请求超过限制的错误,编写实现程序的步骤如下:
(1)在任意目录下创建 server 和 client 项目并初始化( go mod init server / client
),分别在服务端和客户端目录下创建一个 proto 目录,编写 hello.proto
文件如下内容:
syntax = "proto3"; // 版本声明,使用 Protocol Buffers v3 版本
option go_package = "../proto"; // 指定生成的 Go 代码在项目中的导入路径
package pb; // 包名
// 定义服务
service Greeter {
// SayHello 方法
rpc SayHello (HelloRequest) returns (HelloResponse) {}
}
// 请求消息
message HelloRequest {
string name = 1;
}
// 响应消息
message HelloResponse {
string reply = 1;
}
此时,项目的目录结构如下所示:
ErrorHandle
├── client
│ ├── go.mod
│ ├── go.sum
│ └── proto
│ └── hello.proto
└── server
├── go.mod
├── go.sum
└── proto
└── hello.proto
生成 gRPC 源码程序,分别在客户端和服务端项目下的 proto 目录下执行以下的命令:
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto
成功生成后项目的目录结构如下所示:
ErrorHandle
├── client
│ ├── go.mod
│ ├── go.sum
│ └── proto
│ ├── hello_grpc.pb.go
│ ├── hello.pb.go
│ └── hello.proto
└── server
├── go.mod
├── go.sum
└── proto
├── hello_grpc.pb.go
├── hello.pb.go
└── hello.proto
(2)编写 server 端程序使用 map 存储每个 name 的请求次数,超过 1 次则返回错误,并且记录错误详情,该程序的具体代码如下:
package main
import (
"context"
"fmt"
pb "server/proto"
"net"
"sync"
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// grpc server
type server struct {
pb.UnimplementedGreeterServer
mu sync.Mutex // count 的并发锁
count map[string]int // 记录每个 name 的请求次数
}
// SayHello 是需要实现的方法对外提供的服务
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
s.mu.Lock()
defer s.mu.Unlock()
s.count[in.Name]++ // 记录用户的请求次数
// 超过 1 次就返回错误
if s.count[in.Name] > 1 {
st := status.New(codes.ResourceExhausted, "Request limit exceeded.")
ds, err := st.WithDetails(
&errdetails.QuotaFailure{
Violations: []*errdetails.QuotaFailure_Violation{{
Subject: fmt.Sprintf("name:%s", in.Name),
Description: "限制每个 name 只能调用一次!",
}},
},
)
if err != nil {
return nil, st.Err()
}
return nil, ds.Err()
}
// 正常返回响应
reply := "hello " + in.GetName()
return &pb.HelloResponse{Reply: reply}, nil
}
func main() {
// 启动服务
l, err := net.Listen("tcp", ":8972")
if err != nil {
fmt.Printf("failed to listen, err:%v\n", err)
return
}
s := grpc.NewServer() // 创建 gRPC 服务
// 注册服务,注意初始化 count
pb.RegisterGreeterServer(s, &server{count: make(map[string]int)})
// 启动服务
err = s.Serve(l)
if err != nil {
fmt.Printf("failed to serve,err:%v\n", err)
return
}
}
(3)编写 client 端程序,当服务端返回错误时,尝试从错误中获取 detail 信息,该程序的具体代码如下:
package main
import (
"context"
"flag"
"fmt"
"google.golang.org/grpc/status"
pb "client/proto"
"log"
"time"
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
// gRPC 客户端
// 调用 server 端的 SayHello() 方法
var name = flag.String("name", "cqupthao", "通过 -name 告诉 server 你是谁")
func main() {
flag.Parse() // 解析命令行参数
// 连接 server
conn, err := grpc.Dial("127.0.0.1:8972", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("grpc.Dial failed,err:%v", err)
return
}
defer conn.Close()
// 创建客户端
c := pb.NewGreeterClient(conn) // 使用生成的 Go 代码
// 调用 RPC 方法
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
resp, err := c.SayHello(ctx, &pb.HelloRequest{Name: *name})
if err != nil {
s := status.Convert(err) // 将 err 转为 status
for _, d := range s.Details() { // 获取 details
switch info := d.(type) {
case *errdetails.QuotaFailure:
fmt.Printf("Quota failure: %s\n", info)
default:
fmt.Printf("Unexpected type: %s\n", info)
}
}
fmt.Printf("c.SayHello failed, err:%v\n", err)
return
}
// 获取到 RPC 响应
log.Printf("resp:%v\n", resp.GetReply())
}
(4)分别执行服务端和客户端的程序,多次调用服务会输出如下的结果:
// 第一次调用服务
2023/02/15 22:02:41 resp:hello cqupthao
// 第二次调用服务
Quota failure: violations:{subject:"name:cqupthao" description:"限制每个 name 只能调用一次!"}
c.SayHello failed, err:rpc error: code = ResourceExhausted desc = Request limit exceeded.