跳到主要内容

错误处理

本章介绍 gRPC 的错误处理机制,包括标准错误码、错误详情和最佳实践。

gRPC 错误模型

gRPC 定义了一套标准的错误模型,使用状态码和错误消息来表示不同类型的错误:

标准错误码

错误码列表

错误码说明HTTP 映射
OK0成功200
CANCELLED1操作被取消499
UNKNOWN2未知错误500
INVALID_ARGUMENT3客户端传入无效参数400
DEADLINE_EXCEEDED4操作超时504
NOT_FOUND5请求资源不存在404
ALREADY_EXISTS6资源已存在409
PERMISSION_DENIED7权限不足403
RESOURCE_EXHAUSTED8资源耗尽(如配额)429
FAILED_PRECONDITION9前置条件不满足400
ABORTED10操作中止(如并发冲突)409
OUT_OF_RANGE11超出有效范围400
UNIMPLEMENTED12服务未实现501
INTERNAL13服务内部错误500
UNAVAILABLE14服务不可用503
DATA_LOSS15数据丢失或损坏500
UNAUTHENTICATED16未认证401

错误码选择指南

Go 错误处理

返回错误

import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

// 简单错误
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
user, err := s.db.GetUser(req.Id)
if err != nil {
if errors.Is(err, ErrNotFound) {
return nil, status.Error(codes.NotFound, "用户不存在")
}
return nil, status.Error(codes.Internal, "数据库错误")
}
return user, nil
}

// 格式化错误消息
func (s *server) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.User, error) {
if req.Email == "" {
return nil, status.Errorf(codes.InvalidArgument, "邮箱不能为空")
}
// ...
}

错误详情

gRPC 提供了丰富的错误详情类型,用于传递结构化的错误信息。这些类型定义在 google.golang.org/genproto/googleapis/rpc/errdetails 包中。

标准错误详情类型

类型用途场景
BadRequest请求参数错误字段验证失败
PreconditionFailure前置条件不满足资源状态冲突
QuotaFailure配额超限API 调用次数限制
ErrorInfo结构化错误信息特定错误码和元数据
RetryInfo重试建议服务暂时不可用
DebugInfo调试信息开发环境错误追踪
LocalizedMessage本地化消息多语言错误提示
ResourceInfo资源信息资源操作失败
RequestInfo请求信息请求追踪

BadRequest - 参数验证错误

import (
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc/status"
)

func (s *server) CreateOrder(ctx context.Context, req *pb.CreateOrderRequest) (*pb.Order, error) {
// 验证请求
var violations []*errdetails.BadRequest_FieldViolation

if req.ProductId == "" {
violations = append(violations, &errdetails.BadRequest_FieldViolation{
Field: "product_id",
Description: "产品 ID 不能为空",
})
}

if req.Quantity <= 0 {
violations = append(violations, &errdetails.BadRequest_FieldViolation{
Field: "quantity",
Description: "数量必须大于 0",
})
}

if len(violations) > 0 {
badRequest := &errdetails.BadRequest{
FieldViolations: violations,
}

st, err := status.New(codes.InvalidArgument, "参数验证失败").
WithDetails(badRequest)
if err != nil {
return nil, status.Error(codes.Internal, "创建错误详情失败")
}
return nil, st.Err()
}

// 创建订单...
return order, nil
}

PreconditionFailure - 前置条件失败

用于表示请求的前置条件不满足,例如并发冲突或资源状态不符合预期:

func (s *server) UpdateProduct(ctx context.Context, req *pb.UpdateProductRequest) (*pb.Product, error) {
product, err := s.repo.GetProduct(req.Id)
if err != nil {
return nil, err
}

// 乐观锁检查
if product.Version != req.ExpectedVersion {
st, _ := status.New(codes.FailedPrecondition, "并发修改冲突").WithDetails(
&errdetails.PreconditionFailure{
Violations: []*errdetails.PreconditionFailure_Violation{
{
Type: "VERSION_MISMATCH",
Subject: fmt.Sprintf("products/%s", req.Id),
Description: fmt.Sprintf("期望版本 %d,当前版本 %d", req.ExpectedVersion, product.Version),
},
},
},
)
return nil, st.Err()
}

// 更新产品...
return product, nil
}

QuotaFailure - 配额超限

用于表示资源配额耗尽:

func (s *server) CreateResource(ctx context.Context, req *pb.CreateRequest) (*pb.Resource, error) {
// 检查配额
usage, limit, err := s.quotaChecker.Check(req.UserId)
if err != nil {
return nil, err
}

if usage >= limit {
st, _ := status.New(codes.ResourceExhausted, "资源配额耗尽").WithDetails(
&errdetails.QuotaFailure{
Violations: []*errdetails.QuotaFailure_Violation{
{
Subject: req.UserId,
Description: fmt.Sprintf("已使用 %d/%d,请稍后重试或升级套餐", usage, limit),
},
},
},
&errdetails.RetryInfo{
RetryDelay: durationpb.New(time.Minute * 5),
},
)
return nil, st.Err()
}

// 创建资源...
return resource, nil
}

RetryInfo - 重试建议

告诉客户端何时可以重试:

func (s *server) HeavyOperation(ctx context.Context, req *pb.Request) (*pb.Response, error) {
// 检查服务负载
if s.loadMonitor.IsOverloaded() {
st, _ := status.New(codes.Unavailable, "服务繁忙").WithDetails(
&errdetails.RetryInfo{
RetryDelay: durationpb.New(30 * time.Second),
},
)
return nil, st.Err()
}

// 执行操作...
return response, nil
}

ErrorInfo - 结构化错误信息

用于传递特定于应用的错误码和元数据:

func (s *server) Transfer(ctx context.Context, req *pb.TransferRequest) (*pb.TransferResponse, error) {
// 业务逻辑错误
if insufficientFunds {
st, _ := status.New(codes.FailedPrecondition, "余额不足").WithDetails(
&errdetails.ErrorInfo{
Reason: "INSUFFICIENT_FUNDS",
Domain: "banking.example.com",
Metadata: map[string]string{
"account_id": req.FromAccount,
"available": fmt.Sprintf("%.2f", available),
"requested": fmt.Sprintf("%.2f", req.Amount),
"suggestion": "请充值后重试",
},
},
)
return nil, st.Err()
}

// 执行转账...
return response, nil
}

ResourceInfo - 资源信息

用于资源相关的错误:

func (s *server) DeleteFile(ctx context.Context, req *pb.DeleteFileRequest) (*pb.Empty, error) {
file, err := s.storage.GetFile(req.FileId)
if err != nil {
st, _ := status.New(codes.NotFound, "文件不存在").WithDetails(
&errdetails.ResourceInfo{
ResourceType: "file",
ResourceName: req.FileId,
Owner: req.UserId,
Description: "文件可能已被删除或您无权访问",
},
)
return nil, st.Err()
}

// 检查权限
if file.OwnerId != req.UserId {
st, _ := status.New(codes.PermissionDenied, "无权删除此文件").WithDetails(
&errdetails.ResourceInfo{
ResourceType: "file",
ResourceName: req.FileId,
Owner: file.OwnerId,
Description: "只有文件所有者才能删除",
},
)
return nil, st.Err()
}

// 删除文件...
return &emptypb.Empty{}, nil
}

组合多种错误详情

可以在一个错误中组合多种详情类型:

func (s *server) ComplexOperation(ctx context.Context, req *pb.Request) (*pb.Response, error) {
// 组合多种错误详情
st, _ := status.New(codes.InvalidArgument, "请求无法处理").WithDetails(
// 参数验证错误
&errdetails.BadRequest{
FieldViolations: []*errdetails.BadRequest_FieldViolation{
{Field: "email", Description: "邮箱格式不正确"},
},
},
// 资源信息
&errdetails.ResourceInfo{
ResourceType: "user",
ResourceName: req.UserId,
},
// 本地化消息
&errdetails.LocalizedMessage{
Locale: "zh-CN",
Message: "请检查您的输入并重试",
},
)
return nil, st.Err()
}

客户端处理错误

客户端需要能够解析和处理服务端返回的各种错误详情:

import (
"google.golang.org/grpc/status"
"google.golang.org/grpc/codes"
"google.golang.org/genproto/googleapis/rpc/errdetails"
)

func callService(client pb.OrderServiceClient) {
order, err := client.CreateOrder(ctx, req)
if err != nil {
handleGRPCError(err)
return
}

log.Printf("订单创建成功: %s", order.Id)
}

// 通用错误处理函数
func handleGRPCError(err error) {
st, ok := status.FromError(err)
if !ok {
log.Printf("非 gRPC 错误: %v", err)
return
}

// 基本错误信息
log.Printf("错误码: %s", st.Code())
log.Printf("错误消息: %s", st.Message())

// 解析错误详情
for _, detail := range st.Details() {
switch d := detail.(type) {
case *errdetails.BadRequest:
// 处理参数验证错误
handleBadRequest(d)

case *errdetails.PreconditionFailure:
// 处理前置条件失败
handlePreconditionFailure(d)

case *errdetails.QuotaFailure:
// 处理配额超限
handleQuotaFailure(d)

case *errdetails.RetryInfo:
// 获取重试建议
handleRetryInfo(d)

case *errdetails.ErrorInfo:
// 处理结构化错误信息
handleErrorInfo(d)

case *errdetails.ResourceInfo:
// 处理资源信息
handleResourceInfo(d)

case *errdetails.DebugInfo:
// 开发环境打印调试信息
log.Printf("调试信息: %s", d.Detail)
log.Printf("堆栈: %v", d.StackEntries)

case *errdetails.LocalizedMessage:
// 本地化消息
log.Printf("[%s] %s", d.Locale, d.Message)
}
}
}

func handleBadRequest(br *errdetails.BadRequest) {
fmt.Println("\n参数验证错误:")
for _, v := range br.FieldViolations {
fmt.Printf(" - 字段 '%s': %s\n", v.Field, v.Description)
}
}

func handlePreconditionFailure(pf *errdetails.PreconditionFailure) {
fmt.Println("\n前置条件失败:")
for _, v := range pf.Violations {
fmt.Printf(" - 类型: %s, 对象: %s\n", v.Type, v.Subject)
fmt.Printf(" 描述: %s\n", v.Description)
}
}

func handleQuotaFailure(qf *errdetails.QuotaFailure) {
fmt.Println("\n配额超限:")
for _, v := range qf.Violations {
fmt.Printf(" - 对象: %s\n", v.Subject)
fmt.Printf(" 描述: %s\n", v.Description)
}
}

func handleRetryInfo(ri *errdetails.RetryInfo) {
if ri.RetryDelay != nil {
delay := ri.RetryDelay.AsDuration()
fmt.Printf("\n建议 %v 后重试\n", delay)
}
}

func handleErrorInfo(ei *errdetails.ErrorInfo) {
fmt.Println("\n错误详情:")
fmt.Printf(" 原因: %s\n", ei.Reason)
fmt.Printf(" 域: %s\n", ei.Domain)
fmt.Println(" 元数据:")
for k, v := range ei.Metadata {
fmt.Printf(" %s: %s\n", k, v)
}
}

func handleResourceInfo(ri *errdetails.ResourceInfo) {
fmt.Println("\n资源信息:")
fmt.Printf(" 类型: %s\n", ri.ResourceType)
fmt.Printf(" 名称: %s\n", ri.ResourceName)
fmt.Printf(" 所有者: %s\n", ri.Owner)
fmt.Printf(" 描述: %s\n", ri.Description)
}

类型安全的错误处理封装

为了简化错误处理,可以创建封装函数:

// 错误处理封装
type GRPCError struct {
Code codes.Code
Message string
Details []interface{}
FieldErrors map[string]string // 字段名 -> 错误信息
RetryAfter time.Duration // 重试等待时间
ErrorInfo *ErrorDetailInfo // 自定义错误信息
}

type ErrorDetailInfo struct {
Reason string
Domain string
Metadata map[string]string
}

func ParseError(err error) *GRPCError {
grpcErr := &GRPCError{
FieldErrors: make(map[string]string),
}

st, ok := status.FromError(err)
if !ok {
grpcErr.Code = codes.Unknown
grpcErr.Message = err.Error()
return grpcErr
}

grpcErr.Code = st.Code()
grpcErr.Message = st.Message()
grpcErr.Details = st.Details()

for _, detail := range st.Details() {
switch d := detail.(type) {
case *errdetails.BadRequest:
for _, v := range d.FieldViolations {
grpcErr.FieldErrors[v.Field] = v.Description
}
case *errdetails.RetryInfo:
if d.RetryDelay != nil {
grpcErr.RetryAfter = d.RetryDelay.AsDuration()
}
case *errdetails.ErrorInfo:
grpcErr.ErrorInfo = &ErrorDetailInfo{
Reason: d.Reason,
Domain: d.Domain,
Metadata: d.Metadata,
}
}
}

return grpcErr
}

// 使用示例
func createUser(client pb.UserServiceClient, req *pb.CreateUserRequest) {
user, err := client.CreateUser(ctx, req)
if err != nil {
grpcErr := ParseError(err)

// 检查特定字段错误
if emailErr, ok := grpcErr.FieldErrors["email"]; ok {
fmt.Printf("邮箱错误: %s\n", emailErr)
}

// 检查是否可以重试
if grpcErr.RetryAfter > 0 {
fmt.Printf("请在 %v 后重试\n", grpcErr.RetryAfter)
}

return
}

fmt.Printf("用户创建成功: %s\n", user.Id)
}

Python 错误处理

返回错误

from grpc import StatusCode
from grpc_interceptor.exception import GrpcException

class OrderService:
def CreateOrder(self, request, context):
if not request.product_id:
context.abort(StatusCode.INVALID_ARGUMENT, "产品 ID 不能为空")

try:
order = create_order(request)
except ProductNotFoundError:
context.abort(StatusCode.NOT_FOUND, "产品不存在")
except Exception as e:
context.abort(StatusCode.INTERNAL, f"内部错误: {e}")

return order

处理错误

import grpc

def call_service(client):
try:
order = client.CreateOrder(request)
print(f"订单创建成功: {order.id}")
except grpc.RpcError as e:
code = e.code()
message = e.details()

if code == grpc.StatusCode.INVALID_ARGUMENT:
print(f"参数错误: {message}")
elif code == grpc.StatusCode.NOT_FOUND:
print(f"资源不存在: {message}")
else:
print(f"错误: {code} - {message}")

错误重试策略

可重试的错误码

以下错误码通常可以安全重试:

  • CANCELLED - 操作被取消
  • DEADLINE_EXCEEDED - 超时
  • RESOURCE_EXHAUSTED - 资源耗尽
  • ABORTED - 操作中止
  • UNAVAILABLE - 服务不可用

客户端重试配置

// Go 客户端重试配置
conn, err := grpc.Dial(
"localhost:50051",
grpc.WithDefaultServiceConfig(`{
"methodConfig": [{
"name": [{"service": ""}],
"retryPolicy": {
"maxAttempts": 3,
"initialBackoff": "0.1s",
"maxBackoff": "1s",
"backoffMultiplier": 2,
"retryableStatusCodes": ["UNAVAILABLE", "DEADLINE_EXCEEDED"]
}
}]
}`),
)

手动重试

func callWithRetry(client pb.GreeterClient, req *pb.HelloRequest) (*pb.HelloReply, error) {
var lastErr error

for i := 0; i < 3; i++ {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()

resp, err := client.SayHello(ctx, req)
if err == nil {
return resp, nil
}

st, ok := status.FromError(err)
if !ok {
return nil, err
}

// 检查是否可重试
switch st.Code() {
case codes.Unavailable, codes.DeadlineExceeded:
lastErr = err
time.Sleep(time.Second * time.Duration(i+1))
continue
default:
return nil, err
}
}

return nil, lastErr
}

最佳实践

1. 使用具体的错误码

// ❌ 不好:所有错误都用 INTERNAL
return nil, status.Error(codes.Internal, "错误")

// ✅ 好:使用具体的错误码
if user == nil {
return nil, status.Error(codes.NotFound, "用户不存在")
}
if !hasPermission {
return nil, status.Error(codes.PermissionDenied, "无权访问")
}

2. 提供有意义的错误消息

// ❌ 不好:错误消息太笼统
return nil, status.Error(codes.InvalidArgument, "参数错误")

// ✅ 好:详细的错误信息
return nil, status.Errorf(codes.InvalidArgument, "邮箱格式不正确: %s", req.Email)

3. 使用错误详情传递结构化信息

// 使用标准错误详情
st, _ := status.New(codes.FailedPrecondition, "操作失败").
WithDetails(&errdetails.PreconditionFailure{
Violations: []*errdetails.PreconditionFailure_Violation{{
Type: "Stock",
Subject: "product_123",
Description: "库存不足",
}},
})
return nil, st.Err()

4. 区分客户端和服务端错误

func (s *server) Process(ctx context.Context, req *pb.Request) (*pb.Response, error) {
// 客户端错误(4xx)
if err := validateRequest(req); err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}

// 服务端错误(5xx)
result, err := s.processInternal(req)
if err != nil {
log.Printf("内部处理错误: %v", err)
return nil, status.Error(codes.Internal, "处理失败")
}

return result, nil
}

小结

本章我们学习了:

  1. 标准错误码:gRPC 定义的 17 种错误码
  2. 返回错误:使用 status 包创建错误
  3. 错误详情:传递结构化的错误信息
  4. 错误处理:客户端解析和处理错误
  5. 重试策略:处理临时性错误
  6. 最佳实践:使用具体错误码、提供有意义消息

掌握错误处理是构建健壮 gRPC 服务的关键。