文章目录
- [1. gRPC 拦截器](#1. gRPC 拦截器)
-
- [1.1 简单示例](#1.1 简单示例)
- [1.2 多个拦截器](#1.2 多个拦截器)
- [2. gRPC 错误处理](#2. gRPC 错误处理)
-
- [2.1 错误码 codes](#2.1 错误码 codes)
- [2.2 服务端返回的格式](#2.2 服务端返回的格式)
- [2.3 示例](#2.3 示例)
- [2.4 错误添加细节](#2.4 错误添加细节)
- [2.5 自定义错误返回](#2.5 自定义错误返回)
- [3. 小结](#3. 小结)
本系列文章:
- Java 转 go 学习 - 项目管理
- Java 转 go 学习 - 基本语法
- Java 转 go 学习 - 类型转换
- Java 转 go 学习 - 流程控制结构
- Java 转 go 学习 - 数组和切片
- Java 转 go 学习 - map
- Java 转 go 学习 - 函数(1)
- Java 转 go 学习 - 函数(2)
- Java 转 go 学习 - 结构体
- Java 转 go 学习 - 接口
- Java 转 go 学习 - 并发编程(1)
- Java 转 go 学习 - 并发编程(2)
- Java 转 go 学习 - 并发编程(3)
- Java 转 go 学习 - web 编程
- Java 转 go 学习 - Hertz 学习(1)
- Java 转 go 学习 - Hertz 学习(2)
- Java 转 go 学习 - Redis(1)
- Java 转 go 学习 - Redis(2)
- Java 转 go 学习 - MYSQL
- Java 转 go 学习 - GRPC(1)
- Java 转 go 学习 - GRPC(2)
1. gRPC 拦截器
1.1 简单示例
grpc 提供了 grpc.UnaryInterceptor(serverUnaryInterceptor) 方法,可以注册方法拦截器,这个拦截器的类型是:
go
type UnaryServerInterceptor func(ctx context.Context, req any, info *UnaryServerInfo, handler UnaryHandler) (resp any, err error)
下面来看下用法,首先我们看下服务端的拦截器。
go
// serverUnaryInterceptor 会在真正进入 SayHello 前后执行
// 常见用途是记录日志、鉴权、统计耗时、统一处理错误
func serverUnaryInterceptor(
ctx context.Context,
req any, // 客户端请求
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler, // 业务函数
) (any, error) {
start := time.Now()
// 从客户端传来的 metadata 里取 request-id
if md, ok := metadata.FromIncomingContext(ctx); ok {
fmt.Printf("[server interceptor] method=%s request-id=%s\n", info.FullMethod, first(md.Get("request-id")))
}
// 调用真正的业务处理函数
resp, err := handler(ctx, req)
fmt.Printf("[server interceptor] cost=%s err=%v\n", time.Since(start), err)
return resp, err
}
服务端的拦截器会从客户端传过来的 metadata 里面取出 request-id,也就是客户端的请求 id,然后将 id 打印出来。
http 里面有 RequestHeader 请求头,gRPC 也有, metadata 就可以理解成 gRPC 里的 请求头/响应头 。而客户端可以通过 metadata.AppendToOutgoingContext(ctx, "request-id", "req-20260416-001") 将 request-id 设置到 metadata 中,服务端再通过 metadata.FromIncomingContext(ctx) 取出来。
gRPC 的 metadata 本质上是 mapstring\[\]string,也就是一个 key 可以对应多个 value。
下面是客户端的拦截器函数。
go
type UnaryClientInterceptor func(ctx context.Context, method string, req, reply any, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error
具体实现如下。
go
// clientUnaryInterceptor 会包住客户端的每次一元 RPC 调用。
// 这里演示给请求统一追加 request-id,并在调用前后打印日志。
func clientUnaryInterceptor(
ctx context.Context, // 本次 RPC 调用的上下文
method string, // 当前调用的方法名全名
req any, // 也就是发给服务端的 protobuf 消息
reply any, // 响应对象,用来接收服务端返回结果
cc *grpc.ClientConn, // 当前客户端连接对象, 一般业务用不到
invoker grpc.UnaryInvoker, // 执行器, 调用下一个环节,最终把请求发到服务端
opts ...grpc.CallOption, // 附加参数
) error {
// 将 kv 设置到 context 中
ctx = metadata.AppendToOutgoingContext(ctx, "request-id", "req-20260416-001")
fmt.Printf("[client interceptor] before method=%s\n", method)
err := invoker(ctx, method, req, reply, cc, opts...)
fmt.Printf("[client interceptor] after method=%s err=%v\n", method, err)
return err
}
拦截器的各个参数我也写到注释上了,AppendToOutgoingContext 方法中可以把 k-v 设置到 context 中。
同样的在 NewClient 创建客户端的时候可以使用 grpc.WithUnaryInterceptor(clientUnaryInterceptor) 将拦截器作为 opts 附加参数传进去。
最后我们来看下主函数的示例,主函数我们要用到 gRPC 提供的一个工具 bufconn,bufconn 是 gRPC 提供的一个 内存里的假连接,不走真实的 TCP 端口,不经过网络栈,直接在同一个进程里让 client 和 server 通信,比较适合下面这些场景。
- 写示例
- 写单元测试
- 验证拦截器、middleware、序列化逻辑
- 避免占用本地端口
下面看下 proto 文件和生成的 grpc 接口。
go
syntax = "proto3";
package greeter.v1;
option go_package = "example.com/grpc-05/api/proto/greeter/v1;greeterv1";
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
service Greeter {
rpc SayHello(HelloRequest) returns (HelloReply);
}
文件结构如下,生成的 go 代码就不看了。

然后来看下 main 方法。
go
package main
import (
"context"
"fmt"
"log"
"net"
"time"
greeterv1 "example.com/grpc-05/api/proto/greeter/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/test/bufconn"
)
const bufSize = 1024 * 1024
type greeterServer struct {
greeterv1.UnimplementedGreeterServer
}
// SayHello 是业务处理函数,本身只负责处理请求,不关心日志、耗时这类横切逻辑。
func (s *greeterServer) SayHello(ctx context.Context, req *greeterv1.HelloRequest) (*greeterv1.HelloReply, error) {
return &greeterv1.HelloReply{
Message: "Hello, " + req.GetName(),
}, nil
}
// serverUnaryInterceptor 会在真正进入 SayHello 前后执行
// 常见用途是记录日志、鉴权、统计耗时、统一处理错误
func serverUnaryInterceptor(
ctx context.Context,
req any, // 客户端请求
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler, // 业务函数
) (any, error) {
...
}
// clientUnaryInterceptor 会包住客户端的每次一元 RPC 调用。
// 这里演示给请求统一追加 request-id,并在调用前后打印日志。
func clientUnaryInterceptor(
ctx context.Context, // 本次 RPC 调用的上下文
method string, // 当前调用的方法名全名
req any, // 也就是发给服务端的 protobuf 消息
reply any, // 响应对象,用来接收服务端返回结果
cc *grpc.ClientConn, // 当前客户端连接对象, 一般业务用不到
invoker grpc.UnaryInvoker, // 执行器, 调用下一个环节,最终把请求发到服务端
opts ...grpc.CallOption, // 附加参数
) error {
...
}
func first(values []string) string {
if len(values) == 0 {
return ""
}
return values[0]
}
func main() {
// bufconn 提供内存中的连接,适合示例和测试,不需要真实监听 TCP 端口。
lis := bufconn.Listen(bufSize)
// 给服务端挂上一元拦截器。
server := grpc.NewServer(grpc.UnaryInterceptor(serverUnaryInterceptor))
greeterv1.RegisterGreeterServer(server, &greeterServer{})
go func() {
if err := server.Serve(lis); err != nil {
log.Fatalf("server exited: %v", err)
}
}()
defer server.Stop()
// 创建客户端连接,并给客户端挂上一元拦截器。
conn, err := grpc.NewClient(
"passthrough:///bufnet",
grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
return lis.Dial()
}),
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithUnaryInterceptor(clientUnaryInterceptor),
)
if err != nil {
log.Fatalf("create client failed: %v", err)
}
defer conn.Close()
client := greeterv1.NewGreeterClient(conn)
// 调用顺序是:
// client interceptor -> server interceptor -> SayHello -> server interceptor -> client interceptor
resp, err := client.SayHello(context.Background(), &greeterv1.HelloRequest{Name: "Tom"})
if err != nil {
log.Fatalf("say hello failed: %v", err)
}
fmt.Printf("rpc response: %s\n", resp.GetMessage())
}
SayHello 就是核心的 grpc 业务接口,在 main 方法中创建出服务端拦截器之后通过 RegisterGreeterServer 将这个拦截器注册到 greeterv1 这个 grpc 服务中,客户端也同理,而下面是客户端其他参数的意思。
lis.Dial():这个向这个 bufconn 监听器申请一个客户端连接,并把这个连接返回给 gRPC 客户端使用,也就是内存里面的连接而不是真正的 tcp 连接。passthrough:不要做额外名字解析,目标字符串基本原样往下传(后面的 bufnet),比如说如果我们NewClient方法第一个目标参数填的是 localhost:8080 ,那么 gRPC 会把它当成一个目标地址,交给内部的 resolver 去处理,然后再决定怎么拨号连接,这里的 passthrough 就是在告诉 gRPC 别帮我做复杂解析,后面的目标值直接往下传就行 ,正常来说连接建立的逻辑是第一步先看目标字符串怎么解析,第二步再决定怎么建立连接,passthrough 就是让第一步简单点,而第二部建立连接由于我们用了lis.Dial(),所以 passthrough:///bufnet 后面的 bufnet 实际上也没有用到,这里只是为了规范。bufnet:目标名,占位用的,不是必须,改成其他也行,一般来说生产环境有可能会接入北极星,然后写法就变成了polaris://namespace/service-name,当然不同项目写法不同,这种情况下就会去解析后面的 namespace 和 service-name,再调用具体的服务。grpc.WithTransportCredentials(insecure.NewCredentials()):告诉 gRPC,这条连接不使用 TLS,走明文连接,也就是说,它是客户端的传输层安全配置。这里就等于直接告诉服务端我知道这次连接不做证书校验,不做 TLS 握手,直接用非加密连接。因为 gRPC Go 新版客户端创建连接时,必须明确指定 用什么传输凭证,要么就是 TLS,要么就是 insecure,我们本地测试自然不需要加密。
最终来看下 main 方法输出结果。
go
[client interceptor] before method=/greeter.v1.Greeter/SayHello
[server interceptor] method=/greeter.v1.Greeter/SayHello request-id=req-20260416-001
[server interceptor] cost=0s err=<nil>
[client interceptor] after method=/greeter.v1.Greeter/SayHello err=<nil>
rpc response: Hello, Tom
这些拦截器的执行顺序是:
- client interceptor -> server interceptor -> SayHello -> server interceptor -> client interceptor
1.2 多个拦截器
上面是单个拦截器的示例,实际上我们可以通过 grpc.ChainUnaryInterceptor 去添加多个服务端拦截器,也就是拦截器链,也可以通过 WithChainUnaryInterceptor 添加客户端拦截器。
go
server := grpc.NewServer(
// 配置拦截器链
grpc.ChainUnaryInterceptor(
serverLogInterceptor,
serverAuthInterceptor,
),
)
conn, err := grpc.NewClient(
"passthrough:///bufnet",
grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
return lis.Dial()
}),
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithChainUnaryInterceptor(
clientLogInterceptor,
clientMetadataInterceptor,
),
)
下面直接看 main 方法。
go
package main
import (
"context"
"fmt"
"log"
"net"
"time"
greeterv1 "example.com/grpc-05/api/proto/greeter/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/test/bufconn"
)
const bufSize = 1024 * 1024
type greeterServer struct {
greeterv1.UnimplementedGreeterServer
}
// SayHello 是真正的业务处理函数。
// 拦截器会在它的外层包一层或多层,用来处理日志、鉴权、埋点等通用逻辑。
func (s *greeterServer) SayHello(ctx context.Context, req *greeterv1.HelloRequest) (*greeterv1.HelloReply, error) {
return &greeterv1.HelloReply{
Message: "Hello, " + req.GetName(),
}, nil
}
// serverLogInterceptor 是服务端日志拦截器。
// 它在进入业务函数前打印方法名,在业务返回后统计耗时和错误。
func serverLogInterceptor(
ctx context.Context,
req any,
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (any, error) {
start := time.Now()
fmt.Printf("[server log] before method=%s\n", info.FullMethod)
resp, err := handler(ctx, req)
fmt.Printf("[server log] after method=%s cost=%s err=%v\n", info.FullMethod, time.Since(start), err)
return resp, err
}
// serverAuthInterceptor 模拟服务端从 metadata 里读取请求附加信息
// 真实项目里这里常放鉴权、租户校验、灰度标识读取等逻辑
func serverAuthInterceptor(
ctx context.Context,
req any,
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (any, error) {
if md, ok := metadata.FromIncomingContext(ctx); ok {
fmt.Printf("[server auth] request-id=%s\n", first(md.Get("request-id")))
}
res, err := handler(ctx, req)
fmt.Println("[server auth] serverAuthInterceptor end")
return res, err
}
// clientLogInterceptor 是客户端日志拦截器。
// 它会在 RPC 发出前后分别打印一次日志。
func clientLogInterceptor(
ctx context.Context,
method string,
req any,
reply any,
cc *grpc.ClientConn,
invoker grpc.UnaryInvoker,
opts ...grpc.CallOption,
) error {
fmt.Printf("[client log] before method=%s\n", method)
err := invoker(ctx, method, req, reply, cc, opts...)
fmt.Printf("[client log] after method=%s err=%v\n", method, err)
return err
}
// clientMetadataInterceptor 给每次发出的 RPC 统一追加 metadata
// 这里追加的是 request-id,服务端可以从 context 里取出来
func clientMetadataInterceptor(
ctx context.Context,
method string,
req any,
reply any,
cc *grpc.ClientConn,
invoker grpc.UnaryInvoker,
opts ...grpc.CallOption,
) error {
ctx = metadata.AppendToOutgoingContext(ctx, "request-id", "req-20260418-001")
fmt.Printf("[client metadata] append request-id for method=%s\n", method)
return invoker(ctx, method, req, reply, cc, opts...)
}
func first(values []string) string {
if len(values) == 0 {
return ""
}
return values[0]
}
func main() {
// bufconn 提供内存中的连接,适合示例和测试。
// 它不需要真实 TCP 端口,但客户端和服务端的调用链仍然是完整的 gRPC 调用链。
lis := bufconn.Listen(bufSize)
// 服务端多个拦截器按注册顺序组成一条链:
// server log -> server auth -> SayHello -> server auth 返回 -> server log 返回
server := grpc.NewServer(
grpc.ChainUnaryInterceptor(
serverLogInterceptor,
serverAuthInterceptor),
)
greeterv1.RegisterGreeterServer(server, &greeterServer{})
go func() {
if err := server.Serve(lis); err != nil {
log.Fatalf("server exited: %v", err)
}
}()
defer server.Stop()
// 客户端多个拦截器也是按注册顺序组成一条链:
// client log -> client metadata -> 真正发请求 -> client metadata 返回 -> client log 返回
conn, err := grpc.NewClient(
"passthrough:///bufnet",
grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
return lis.Dial()
}),
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithChainUnaryInterceptor(
clientLogInterceptor,
clientMetadataInterceptor,
),
)
if err != nil {
log.Fatalf("create client failed: %v", err)
}
defer conn.Close()
client := greeterv1.NewGreeterClient(conn)
// 这里触发一次普通的一元 RPC。
// 调用时会先经过客户端拦截器,再进入服务端拦截器,最后才到业务函数 SayHello。
resp, err := client.SayHello(context.Background(), &greeterv1.HelloRequest{Name: "Tom"})
if err != nil {
log.Fatalf("say hello failed: %v", err)
}
fmt.Printf("rpc response: %s\n", resp.GetMessage())
}
服务端的拦截器顺序是:
- serverLogInterceptor
- serverAuthInterceptor
客户端的拦截器顺序是:
- clientLogInterceptor
- clientMetadataInterceptor
所以整体的拦截器执行顺序应该是:
go
clientLogInterceptor
-> clientMetadataInterceptor
-> 发起 RPC
-> serverLogInterceptor
-> serverAuthInterceptor
-> SayHello
-> serverAuthInterceptor 返回
-> serverLogInterceptor 返回
-> clientMetadataInterceptor 返回
-> clientLogInterceptor 返回
事实也是,main 方法的输出结果如下。
go
[client log] before method=/greeter.v1.Greeter/SayHello
[client metadata] append request-id for method=/greeter.v1.Greeter/SayHello
[server log] before method=/greeter.v1.Greeter/SayHello
[server auth] request-id=req-20260418-001
[server auth] serverAuthInterceptor end
[server log] after method=/greeter.v1.Greeter/SayHello cost=0s err=<nil>
[client log] after method=/greeter.v1.Greeter/SayHello err=<nil>
rpc response: Hello, Tom
2. gRPC 错误处理
gRPC 不推荐直接返回 error,有一套官方的标准:
status.Error+codes错误码
2.1 错误码 codes
错误码 codes 常用的如下。
| 错误码 | 含义 | 常见场景 |
|---|---|---|
codes.OK |
请求成功,没有错误 | 正常返回业务结果时使用 |
codes.Canceled |
请求被取消 | 客户端主动取消请求,或上游 context 被取消 |
codes.Unknown |
未知错误 | 服务端返回了无法明确分类的错误,通常不建议业务主动使用 |
codes.InvalidArgument |
请求参数不合法 | 参数缺失、格式错误、范围错误,比如 user_id <= 0 |
codes.DeadlineExceeded |
请求超时 | 客户端设置了超时,服务端没在截止时间前处理完 |
codes.NotFound |
资源不存在 | 用户不存在、订单不存在、配置不存在 |
codes.AlreadyExists |
资源已存在 | 创建用户时用户名已存在,创建订单号重复 |
codes.PermissionDenied |
没有权限 | 用户身份没问题,但没有操作当前资源的权限 |
codes.Unauthenticated |
未认证 | 没登录、token 缺失、token 无效、签名失败 |
codes.ResourceExhausted |
资源不足或超限 | 限流、配额超限、连接池满、磁盘/内存资源不足 |
codes.FailedPrecondition |
当前状态不满足执行条件 | 订单未支付不能发货,账户未实名认证不能提现吗 |
codes.Aborted |
操作被中止,通常和并发冲突有关 | 乐观锁冲突、事务冲突、重复提交需要回滚 |
codes.OutOfRange |
请求超出合法范围 | 分页越界、读取偏移量越界、索引超范围 |
codes.Unimplemented |
方法或能力未实现 | RPC 还没开发、某功能暂不支持 |
codes.Internal |
服务端内部错误 | 代码 bug、内部状态异常、依赖结果不符合预期 |
codes.Unavailable |
服务暂时不可用 | 服务实例挂了、网络抖动、下游不可达、服务正在重启 |
codes.DataLoss |
数据损坏或不可恢复 | 数据校验失败、存储内容损坏、关键数据丢失 |
2.2 服务端返回的格式
go
return nil, status.Error(错误码, "错误描述信息")
2.3 示例
同样的,我们可以来看下示例,首先就是 proto 文件。
go
syntax = "proto3";
package user.v1;
option go_package = "example.com/grpc-06/api/proto/user/v1;userv1";
message GetUserRequest {
uint64 user_id = 1;
}
message GetUserReply {
uint64 user_id = 1;
string name = 2;
}
service UserService {
rpc GetUser(GetUserRequest) returns (GetUserReply);
}
生成的 go 文件就不看了,整个文件的结构如下。

gRPC 接口是 GetUser,下面是服务端的接口,通过 gRPC 的 status.Error 直接返回 Error 结构体错误,也就是下面这个结构体。
go
type Error struct {
s *Status
}
下面看服务端代码。
go
type userServer struct {
userv1.UnimplementedUserServiceServer
}
// GetUser demonstrates typical gRPC error handling on the server side.
func (s *userServer) GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.GetUserReply, error) {
switch req.GetUserId() {
case 1:
return &userv1.GetUserReply{
UserId: 1,
Name: "Tom",
}, nil
case 0:
return nil, status.Error(codes.InvalidArgument, "user_id must be greater than 0")
case 404:
return nil, status.Error(codes.NotFound, "user not found")
case 500:
return nil, status.Error(codes.Internal, "internal server error")
default:
return nil, status.Error(codes.PermissionDenied, "no permission to access this user")
}
}
然后通过下面的方法去处理错误,首先通过 status.FromError 解析这个 Error 结构体获取 Status,再对 Code 属性分别讨论。
go
func callAndPrint(client userv1.UserServiceClient, userID uint64) {
fmt.Printf("call GetUser user_id=%d\n", userID)
resp, err := client.GetUser(context.Background(), &userv1.GetUserRequest{
UserId: userID,
})
if err != nil {
st, ok := status.FromError(err)
if !ok {
fmt.Printf("non-gRPC error: %v\n\n", err)
return
}
fmt.Printf("grpc error code=%s message=%s\n", st.Code(), st.Message())
switch st.Code() {
case codes.InvalidArgument:
fmt.Println("client action: fix request parameters")
case codes.NotFound:
fmt.Println("client action: show user not found")
case codes.Internal:
fmt.Println("client action: retry later or alert")
case codes.PermissionDenied:
fmt.Println("client action: deny operation")
default:
fmt.Println("client action: generic fallback")
}
fmt.Println()
return
}
fmt.Printf("success user_id=%d name=%s\n\n", resp.GetUserId(), resp.GetName())
}
2.4 错误添加细节
上面例子中的 status.Error(...) 是比较常见的错误返回方式,但是这种方式返回的字段只有两个。
- code
- message
但是有时候客户端不单单想要知道一些错误码和消息,也想知道下面的信息。
- 到底哪个字段错了
- 这是哪种稳定错误原因
- 关联的是哪个资源
- 是否可以重试
- 是否有本地化提示文案
这些信息可以放到 details 返回,就可以用到 status.WithDetails,这个方法的签名是。
go
func (s *Status) WithDetails(details ...protoadapt.MessageV1) (*Status, error)
用法如下。
go
// invalidUserIDError 返回一个带详情的 InvalidArgument 错误。
// 顶层的 code/message 适合通用处理;
// details 部分适合客户端做更细粒度的解析。
func invalidUserIDError() error {
// 创建 Status
st := status.New(codes.InvalidArgument, "invalid request parameters")
// 加上细节
withDetails, err := st.WithDetails(
&errdetails.BadRequest{
FieldViolations: []*errdetails.BadRequest_FieldViolation{
{
// 字段
Field: "user_id",
// 字段错误原因
Description: "user_id must be greater than 0",
},
},
},
&errdetails.LocalizedMessage{
// 语言
Locale: "zh-CN",
// 信息
Message: "user_id 必须大于 0",
},
)
if err != nil {
// 如果附加详情失败,则退化成普通 gRPC 错误。
return status.Error(codes.Internal, "build invalid argument details failed")
}
// 返回错误码, 将上面的内容组合在一起
return withDetails.Err()
}
BadRequest 和 LocalizedMessage 都是 gRPC 官方提供的 错误详情类型,一般配合 WithDetails(...) 使用,但它们用途不一样。
- BadRequest 用来表示请求参数哪里不合法。
- LocalizedMessage 用来表达 给用户看的本地化错误提示,告诉前端该怎么向用户展示。
同样的还有 ErrorInfo 和 ResourceInfo ,也都是 结构化错误详情 ,但它们关注的不是 字段校验 ,而是 错误原因和资源上下文。
- ErrorInfo 用来表示 这个错误的稳定业务原因是什么,比如权限校验失败,错误原因是非管理员身份,还有比如 Token 校验失败等固定的原因,这个跟 message 的区别就是这个要求返回的原因尽量文档,也就是一种错误码的造成方式要稳定。
- ResourceInfo 用来表示 这个错误和哪个资源有关。
下面来看下具体的返回例子。
go
// permissionDeniedError 返回一个带 ErrorInfo 详情的 PermissionDenied 错误。
// 当客户端需要根据稳定的 reason 或 metadata 做分支处理时,这种写法更合适。
func permissionDeniedError(userID uint64) error {
st := status.New(codes.PermissionDenied, "no permission to access this user")
withDetails, err := st.WithDetails(
&errdetails.ErrorInfo{
// 稳定原因
Reason: "USER_READ_FORBIDDEN",
// 业务
Domain: "grpc-06.user",
// 附加 k-v
Metadata: map[string]string{"user_id": fmt.Sprintf("%d", userID)},
},
&errdetails.ResourceInfo{
// 资源类型
ResourceType: "user",
// 资源名称
ResourceName: fmt.Sprintf("users/%d", userID),
// 错误描述
Description: "the caller is not allowed to read this user",
},
)
if err != nil {
return status.Error(codes.Internal, "build permission denied details failed")
}
return withDetails.Err()
}
上面这两个 Error 最终返回的都是两部分组成。
- status.New 创建出来的基础信息(code,message)
- 添加的细节信息
比如上面第一个参数错误的就如下所示。

服务端测试,根据不同 userId 发送不同的结果。
go
// GetUser 演示几种常见的 gRPC 错误处理方式:
// 1. 正常返回业务结果
// 2. 直接返回 status.Error(...)
// 3. 通过 status.New(...).WithDetails(...) 返回带结构化详情的错误
func (s *userServer) GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.GetUserReply, error) {
switch req.GetUserId() {
case 1:
return &userv1.GetUserReply{
UserId: 1,
Name: "Tom",
}, nil
case 0:
// InvalidArgument,附带字段级别的校验详情。
return nil, invalidUserIDError()
case 404:
return nil, status.Error(codes.NotFound, "user not found")
case 500:
return nil, status.Error(codes.Internal, "internal server error")
default:
// PermissionDenied,附带便于客户端识别的结构化上下文。
return nil, permissionDeniedError(req.GetUserId())
}
}
客户端下面可以用这个方法来解析 Status。
go
// printDetails 解析并打印通过 WithDetails 附加的结构化错误详情。
func printDetails(st *status.Status) {
for _, detail := range st.Details() {
switch d := detail.(type) {
case *errdetails.BadRequest:
for _, violation := range d.FieldViolations {
fmt.Printf("detail BadRequest field=%s description=%s\n", violation.Field, violation.Description)
}
case *errdetails.LocalizedMessage:
fmt.Printf("detail LocalizedMessage locale=%s message=%s\n", d.Locale, d.Message)
case *errdetails.ErrorInfo:
fmt.Printf("detail ErrorInfo reason=%s domain=%s metadata=%v\n", d.Reason, d.Domain, d.Metadata)
case *errdetails.ResourceInfo:
fmt.Printf(
"detail ResourceInfo type=%s name=%s description=%s\n",
d.ResourceType,
d.ResourceName,
d.Description,
)
default:
fmt.Printf("detail %T\n", d)
}
}
}
最后看下 main 方法。
go
func main() {
lis := bufconn.Listen(bufSize)
server := grpc.NewServer()
userv1.RegisterUserServiceServer(server, &userServer{})
go func() {
if err := server.Serve(lis); err != nil {
log.Fatalf("server exited: %v", err)
}
}()
defer server.Stop()
conn, err := grpc.NewClient(
"passthrough:///bufnet",
grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
return lis.Dial()
}),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatalf("create client failed: %v", err)
}
defer conn.Close()
client := userv1.NewUserServiceClient(conn)
callAndPrint(client, 1)
callAndPrint(client, 0)
callAndPrint(client, 404)
callAndPrint(client, 500)
callAndPrint(client, 999)
}
func callAndPrint(client userv1.UserServiceClient, userID uint64) {
fmt.Printf("call GetUser user_id=%d\n", userID)
resp, err := client.GetUser(context.Background(), &userv1.GetUserRequest{
UserId: userID,
})
if err != nil {
st, ok := status.FromError(err)
if !ok {
fmt.Printf("non-gRPC error: %v\n\n", err)
return
}
fmt.Printf("grpc error code=%s message=%s\n", st.Code(), st.Message())
printDetails(st)
switch st.Code() {
case codes.InvalidArgument:
fmt.Println("client action: fix request parameters")
case codes.NotFound:
fmt.Println("client action: show user not found")
case codes.Internal:
fmt.Println("client action: retry later or alert")
case codes.PermissionDenied:
fmt.Println("client action: deny operation")
default:
fmt.Println("client action: generic fallback")
}
fmt.Println()
return
}
fmt.Printf("success user_id=%d name=%s\n\n", resp.GetUserId(), resp.GetName())
}
打印结果如下。
go
call GetUser user_id=1
success user_id=1 name=Tom
call GetUser user_id=0
grpc error code=InvalidArgument message=invalid request parameters
detail BadRequest field=user_id description=user_id must be greater than 0
detail LocalizedMessage locale=zh-CN message=user_id 必须大于 0
client action: fix request parameters
call GetUser user_id=404
grpc error code=NotFound message=user not found
client action: show user not found
call GetUser user_id=500
grpc error code=Internal message=internal server error
client action: retry later or alert
call GetUser user_id=999
grpc error code=PermissionDenied message=no permission to access this user
detail ErrorInfo reason=USER_READ_FORBIDDEN domain=grpc-06.user metadata=map[user_id:999]
detail ResourceInfo type=user name=users/999 description=the caller is not allowed to read this user
client action: deny operation
2.5 自定义错误返回
上面我们在 WithDetails 中用的都是 gRPC 提供的结构体,我们也可以用自定义的结构体,但是有一个问题,这个方法传入的是 MessageV1 接口的实现类。
go
type MessageV1 interface {
Reset()
String() string
ProtoMessage()
}
所以如果我们要用自定义结构体就也要实现这几个方法,但是我们可以把结构体定义到 proto 文件中,然后用 proto 生成 go 文件,这样定义出来并生成的 protobuf 类型就默认实现了这三个接口,可以直接使用。
go
message UserErrorDetail {
string reason = 1;
uint32 biz_code = 2;
string biz_message = 3;
string help = 4;
}
在代码中就可以直接用。
go
func quotaExceededError(userID uint64) error {
st := status.New(codes.ResourceExhausted, "user quota exceeded")
withDetails, err := st.WithDetails(&userv1.UserErrorDetail{
Reason: "USER_QUOTA_EXCEEDED",
BizCode: 10001,
BizMessage: "current caller has exceeded the user read quota",
Help: fmt.Sprintf("reduce request rate and retry later, user_id=%d", userID),
})
if err != nil {
return status.Error(codes.Internal, "build custom details failed")
}
return withDetails.Err()
}
客户端解析就加上这个类型。
go
// printDetails 解析并打印通过 WithDetails 附加的结构化错误详情。
func printDetails(st *status.Status) {
for _, detail := range st.Details() {
switch d := detail.(type) {
...
case *userv1.UserErrorDetail:
fmt.Printf(
"detail UserErrorDetail reason=%s biz_code=%d biz_message=%s help=%s\n",
d.Reason,
d.BizCode,
d.BizMessage,
d.Help,
)
default:
fmt.Printf("detail %T\n", d)
}
}
}
3. 小结
这篇文章就先到这里,下一篇继续学习 gRPC 如何生成 http 接口。