错误处理与状态码
2026/1/15大约 4 分钟gRPC错误处理Status
错误处理与状态码
一、gRPC 状态码
1.1 状态码列表
| 状态码 | 名称 | 说明 |
|---|---|---|
| 0 | OK | 成功 |
| 1 | CANCELLED | 操作被取消 |
| 2 | UNKNOWN | 未知错误 |
| 3 | INVALID_ARGUMENT | 无效参数 |
| 4 | DEADLINE_EXCEEDED | 超时 |
| 5 | NOT_FOUND | 资源不存在 |
| 6 | ALREADY_EXISTS | 资源已存在 |
| 7 | PERMISSION_DENIED | 权限不足 |
| 8 | RESOURCE_EXHAUSTED | 资源耗尽 |
| 9 | FAILED_PRECONDITION | 前置条件失败 |
| 10 | ABORTED | 操作中止 |
| 11 | OUT_OF_RANGE | 超出范围 |
| 12 | UNIMPLEMENTED | 未实现 |
| 13 | INTERNAL | 内部错误 |
| 14 | UNAVAILABLE | 服务不可用 |
| 15 | DATA_LOSS | 数据丢失 |
| 16 | UNAUTHENTICATED | 未认证 |
1.2 状态码使用场景
import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// 参数错误
status.Error(codes.InvalidArgument, "用户名不能为空")
// 资源不存在
status.Error(codes.NotFound, "用户不存在")
// 权限不足
status.Error(codes.PermissionDenied, "无权访问")
// 未认证
status.Error(codes.Unauthenticated, "请先登录")
// 内部错误
status.Error(codes.Internal, "服务器内部错误")二、返回错误
2.1 基本错误
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
if req.Id <= 0 {
return nil, status.Error(codes.InvalidArgument, "用户ID必须大于0")
}
user, err := s.db.FindUser(req.Id)
if err != nil {
return nil, status.Error(codes.Internal, "数据库查询失败")
}
if user == nil {
return nil, status.Error(codes.NotFound, "用户不存在")
}
return user, nil
}2.2 带详情的错误
import (
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc/status"
)
func (s *server) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.User, error) {
// 验证参数
var violations []*errdetails.BadRequest_FieldViolation
if req.Username == "" {
violations = append(violations, &errdetails.BadRequest_FieldViolation{
Field: "username",
Description: "用户名不能为空",
})
}
if req.Email == "" {
violations = append(violations, &errdetails.BadRequest_FieldViolation{
Field: "email",
Description: "邮箱不能为空",
})
}
if len(violations) > 0 {
st := status.New(codes.InvalidArgument, "参数验证失败")
br := &errdetails.BadRequest{FieldViolations: violations}
st, _ = st.WithDetails(br)
return nil, st.Err()
}
// 创建用户...
return &pb.User{}, nil
}三、处理错误
3.1 客户端处理
func main() {
resp, err := client.GetUser(ctx, &pb.GetUserRequest{Id: 1})
if err != nil {
// 获取状态
st, ok := status.FromError(err)
if ok {
fmt.Printf("错误码: %v\n", st.Code())
fmt.Printf("错误信息: %s\n", st.Message())
// 根据错误码处理
switch st.Code() {
case codes.NotFound:
fmt.Println("用户不存在")
case codes.InvalidArgument:
fmt.Println("参数错误")
case codes.Unauthenticated:
fmt.Println("请先登录")
default:
fmt.Println("未知错误")
}
}
return
}
fmt.Printf("用户: %s\n", resp.Name)
}3.2 获取错误详情
func handleError(err error) {
st, ok := status.FromError(err)
if !ok {
log.Printf("非 gRPC 错误: %v", err)
return
}
log.Printf("错误码: %v, 信息: %s", st.Code(), st.Message())
// 获取详情
for _, detail := range st.Details() {
switch d := detail.(type) {
case *errdetails.BadRequest:
for _, v := range d.FieldViolations {
log.Printf("字段 %s: %s", v.Field, v.Description)
}
case *errdetails.RetryInfo:
log.Printf("重试延迟: %v", d.RetryDelay.AsDuration())
}
}
}四、自定义错误
4.1 定义错误消息
// error.proto
syntax = "proto3";
package error;
message ErrorInfo {
int32 code = 1;
string message = 2;
map<string, string> metadata = 3;
}4.2 使用自定义错误
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
if req.Id <= 0 {
st := status.New(codes.InvalidArgument, "参数错误")
errorInfo := &pb.ErrorInfo{
Code: 1001,
Message: "用户ID必须大于0",
Metadata: map[string]string{
"field": "id",
"value": fmt.Sprintf("%d", req.Id),
},
}
st, _ = st.WithDetails(errorInfo)
return nil, st.Err()
}
return &pb.User{}, nil
}五、超时处理
5.1 设置超时
// 客户端设置超时
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := client.GetUser(ctx, &pb.GetUserRequest{Id: 1})
if err != nil {
st, _ := status.FromError(err)
if st.Code() == codes.DeadlineExceeded {
log.Println("请求超时")
}
}5.2 服务端检查超时
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
// 检查是否已超时
if ctx.Err() == context.DeadlineExceeded {
return nil, status.Error(codes.DeadlineExceeded, "请求已超时")
}
// 长时间操作前检查
select {
case <-ctx.Done():
return nil, status.Error(codes.Canceled, "请求已取消")
default:
}
// 执行操作...
return &pb.User{}, nil
}六、重试机制
6.1 客户端重试
func callWithRetry(client pb.UserServiceClient, req *pb.GetUserRequest) (*pb.User, error) {
var lastErr error
for i := 0; i < 3; i++ {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
resp, err := client.GetUser(ctx, req)
cancel()
if err == nil {
return resp, nil
}
lastErr = err
st, _ := status.FromError(err)
// 只对特定错误重试
if st.Code() != codes.Unavailable && st.Code() != codes.DeadlineExceeded {
return nil, err
}
// 指数退避
time.Sleep(time.Duration(1<<i) * 100 * time.Millisecond)
}
return nil, lastErr
}6.2 使用 gRPC 重试策略
import "google.golang.org/grpc/serviceconfig"
retryPolicy := `{
"methodConfig": [{
"name": [{"service": "user.UserService"}],
"retryPolicy": {
"maxAttempts": 3,
"initialBackoff": "0.1s",
"maxBackoff": "1s",
"backoffMultiplier": 2,
"retryableStatusCodes": ["UNAVAILABLE", "DEADLINE_EXCEEDED"]
}
}]
}`
conn, _ := grpc.Dial(
"localhost:50051",
grpc.WithInsecure(),
grpc.WithDefaultServiceConfig(retryPolicy),
)七、错误处理最佳实践
7.1 错误包装
type AppError struct {
Code codes.Code
Message string
Err error
}
func (e *AppError) Error() string {
return e.Message
}
func (e *AppError) GRPCStatus() *status.Status {
return status.New(e.Code, e.Message)
}
// 使用
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
user, err := s.db.FindUser(req.Id)
if err != nil {
return nil, &AppError{
Code: codes.Internal,
Message: "查询用户失败",
Err: err,
}
}
if user == nil {
return nil, &AppError{
Code: codes.NotFound,
Message: "用户不存在",
}
}
return user, nil
}7.2 错误日志
func ErrorLoggingInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
resp, err := handler(ctx, req)
if err != nil {
st, _ := status.FromError(err)
log.Printf("[ERROR] 方法: %s, 错误码: %v, 信息: %s",
info.FullMethod, st.Code(), st.Message())
}
return resp, err
}