错误处理
本章介绍 gRPC 的错误处理机制,包括标准错误码、错误详情和最佳实践。
gRPC 错误模型
gRPC 定义了一套标准的错误模型,使用状态码和错误消息来表示不同类型的错误:
标准错误码
错误码列表
| 错误码 | 值 | 说明 | HTTP 映射 |
|---|---|---|---|
OK | 0 | 成功 | 200 |
CANCELLED | 1 | 操作被取消 | 499 |
UNKNOWN | 2 | 未知错误 | 500 |
INVALID_ARGUMENT | 3 | 客户端传入无效参数 | 400 |
DEADLINE_EXCEEDED | 4 | 操作超时 | 504 |
NOT_FOUND | 5 | 请求资源不存在 | 404 |
ALREADY_EXISTS | 6 | 资源已存在 | 409 |
PERMISSION_DENIED | 7 | 权限不足 | 403 |
RESOURCE_EXHAUSTED | 8 | 资源耗尽(如配额) | 429 |
FAILED_PRECONDITION | 9 | 前置条件不满足 | 400 |
ABORTED | 10 | 操作中止(如并发冲突) | 409 |
OUT_OF_RANGE | 11 | 超出有效范围 | 400 |
UNIMPLEMENTED | 12 | 服务未实现 | 501 |
INTERNAL | 13 | 服务内部错误 | 500 |
UNAVAILABLE | 14 | 服务不可用 | 503 |
DATA_LOSS | 15 | 数据丢失或损坏 | 500 |
UNAUTHENTICATED | 16 | 未认证 | 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
}
小结
本章我们学习了:
- 标准错误码:gRPC 定义的 17 种错误码
- 返回错误:使用 status 包创建错误
- 错误详情:传递结构化的错误信息
- 错误处理:客户端解析和处理错误
- 重试策略:处理临时性错误
- 最佳实践:使用具体错误码、提供有意义消息
掌握错误处理是构建健壮 gRPC 服务的关键。