Skip to content

错误处理与状态码

gRPC 的错误处理远不止「返回一个 error」。它有一套跨语言的标准状态码体系,支持携带结构化的错误详情(rich error),能在 HTTP/2 层面传播到任何语言的客户端。本篇深入 gRPC 的状态码语义、status 包的用法、errdetails 丰富错误模式、HTTP 与 gRPC 的状态码映射,以及错误日志与追踪的最佳实践。

一、gRPC 状态码体系

gRPC 定义了 17 个标准状态码,所有语言实现一致。以下是完整列表及其语义:

状态码含义HTTP 映射
OK0成功200
Canceled1客户端取消499
Unknown2服务端返回了无法解析的错误500
InvalidArgument3参数无效400
DeadlineExceeded4超时504
NotFound5资源不存在404
AlreadyExists6资源已存在409
PermissionDenied7无权限403
ResourceExhausted8资源耗尽(限流)429
FailedPrecondition9前置条件不满足400
Aborted10并发冲突(可重试)409
OutOfRange11超出有效范围400
Unimplemented12方法未实现501
Internal13内部错误500
Unavailable14服务不可用(可重试)503
DataLoss15数据丢失500
Unauthenticated16未认证401
go
package main

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

func main() {
	// 状态码是 int32 的别名
	fmt.Printf("OK = %d\n", codes.OK)
	fmt.Printf("NotFound = %d\n", codes.NotFound)
	fmt.Printf("Internal = %d\n", codes.Internal)

	// 状态码可比较
	if codes.NotFound != codes.OK {
		fmt.Println("NotFound is not OK")
	}

	// 状态码转字符串
	fmt.Printf("code 14 = %s\n", codes.Code(14).String())
}

状态码选择要点

  • InvalidArgument vs FailedPrecondition:前者是参数本身非法(如负数 ID),后者是参数合法但当前状态不允许(如订单已发货无法取消)。
  • NotFound vs PermissionDenied:前者是资源不存在,后者是资源存在但无权访问(注意:为防止信息泄露,某些场景应统一返回 NotFound)。
  • Aborted vs FailedPrecondition:前者是并发冲突(如乐观锁失败,可重试),后者是状态前置条件不满足。
  • Unavailable 是唯一默认可重试的状态码。

二、status 包

google.golang.org/grpc/status 包是创建和解析 gRPC 错误的核心工具。

1. 创建错误

go
package main

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

func main() {
	// 方式一:status.Error 创建简单错误
	err1 := status.Error(codes.NotFound, "user 42 not found")
	fmt.Printf("err1: %v\n", err1)

	// 方式二:status.Errorf 带格式化
	err2 := status.Errorf(codes.InvalidArgument, "age %d is out of range [0, 150]", 200)
	fmt.Printf("err2: %v\n", err2)

	// 方式三:status.New 创建 Status 对象,再转 error
	st := status.New(codes.PermissionDenied, "admin role required")
	err3 := st.Err()
	fmt.Printf("err3: %v\n", err3)

	// 方式四:从已有 error 转换
	err4 := status.Convert(fmt.Errorf("some error"))
	fmt.Printf("err4 code: %v\n", err4.Code()) // 默认 Unknown
}

2. 解析错误

go
package main

import (
	"errors"
	"fmt"

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

func main() {
	// 模拟服务端返回的错误
	err := status.Errorf(codes.NotFound, "order %d not found", 12345)

	// 方式一:status.FromError 返回 Status + bool
	st, ok := status.FromError(err)
	if ok {
		fmt.Printf("code: %v\n", st.Code())
		fmt.Printf("message: %s\n", st.Message())
		fmt.Printf("full: %v\n", st)
	}

	// 方式二:status.Code 直接提取状态码
	code := status.Code(err)
	fmt.Printf("code: %v\n", code)

	// 非 gRPC 错误的 Code 是 Unknown
	plainErr := errors.New("plain error")
	fmt.Printf("plain code: %v\n", status.Code(plainErr))

	// 判断特定状态码
	if status.Code(err) == codes.NotFound {
		fmt.Println("-> not found, use default value")
	}

	// errors.Is 与 gRPC 错误
	wrapped := fmt.Errorf("wrap: %w", err)
	fmt.Printf("wrapped code: %v\n", status.Code(wrapped))
	fmt.Printf("is NotFound: %v\n", errors.Is(wrapped, status.Error(codes.NotFound, "")))
}

三、错误详情:errdetails 包

简单的 code + message 不够用——你需要告诉客户端「哪个字段错了」「多久后重试」「需要什么权限」。gRPC 通过 错误详情(Error Details) 机制解决,由 google.golang.org/genproto/googleapis/rpc/errdetails 包提供。

1. ErrorInfo:通用错误信息

go
package main

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

func main() {
	// 创建带 ErrorInfo 详情的错误
	st := status.New(codes.InvalidArgument, "request validation failed")
	st, _ = st.WithDetails(&errdetails.ErrorInfo{
		Reason: "INVALID_EMAIL_FORMAT",
		Domain: "shop.example.com",
		Metadata: map[string]string{
			"field":     "email",
			"input":     "alice@",
			"pattern":   "^[a-z]+@[a-z]+\\.[a-z]+$",
			"doc_url":   "https://docs.example.com/errors/invalid-email",
		},
	})

	err := st.Err()
	fmt.Printf("error: %v\n", err)

	// 客户端解析详情
	if st, ok := status.FromError(err); ok {
		fmt.Printf("code: %v\n", st.Code())
		fmt.Printf("message: %s\n", st.Message())
		for _, d := range st.Details() {
			switch info := d.(type) {
			case *errdetails.ErrorInfo:
				fmt.Printf("ErrorInfo: reason=%s domain=%s metadata=%v\n",
					info.Reason, info.Domain, info.Metadata)
			}
		}
	}
}

2. BadRequest:字段级校验错误

go
package main

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

func main() {
	// 模拟表单校验:多个字段同时出错
	st := status.New(codes.InvalidArgument, "multiple validation errors")
	st, _ = st.WithDetails(&errdetails.BadRequest{
		FieldViolations: []*errdetails.BadRequest_FieldViolation{
			{
				Field:       "email",
				Description: "must be a valid email address",
			},
			{
				Field:       "age",
				Description: "must be between 0 and 150",
			},
			{
				Field:       "password",
				Description: "must be at least 8 characters",
			},
		},
	})

	err := st.Err()

	// 客户端解析字段级错误
	if st, ok := status.FromError(err); ok {
		for _, d := range st.Details() {
			if br, ok := d.(*errdetails.BadRequest); ok {
				for _, v := range br.FieldViolations {
					fmt.Printf("field %q: %s\n", v.Field, v.Description)
				}
			}
		}
	}
}

3. RetryInfo:重试建议

go
package main

import (
	"fmt"
	"time"
	"google.golang.org/genproto/googleapis/rpc/errdetails"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/durationpb"
)

func main() {
	// 服务端告知客户端「限流了,2 秒后重试」
	retryDelay := durationpb.New(2 * time.Second)
	st := status.New(codes.ResourceExhausted, "rate limit exceeded")
	st, _ = st.WithDetails(&errdetails.RetryInfo{
		RetryDelay: retryDelay,
	})

	err := st.Err()

	// 客户端解析重试延迟
	if st, ok := status.FromError(err); ok {
		for _, d := range st.Details() {
			if ri, ok := d.(*errdetails.RetryInfo); ok {
				delay := ri.RetryDelay.AsDuration()
				fmt.Printf("should retry after %v\n", delay)
				// 实际中:time.Sleep(delay) 然后 retry
			}
		}
	}
}

4. 其他常用 errdetails

类型用途
QuotaFailure配额超限,附带哪些资源超限
PreconditionFailure前置条件不满足的详细列表
ErrorInfo通用错误原因、域名、元数据
BadRequest字段级校验错误
RetryInfo重试延迟建议
DebugInfo调试信息(堆栈、异常详情)
LocalizedMessage本地化错误消息
go
package main

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

func main() {
	// QuotaFailure:配额超限
	st1 := status.New(codes.ResourceExhausted, "quota exceeded")
	st1, _ = st1.WithDetails(&errdetails.QuotaFailure{
		Violations: []*errdetails.QuotaFailure_Violation{
			{Subject: "api_calls_per_minute", Description: "exceeded 1000/min limit"},
			{Subject: "storage_bytes", Description: "exceeded 10GB limit"},
		},
	})
	fmt.Printf("quota error: %v\n", st1.Err())

	// PreconditionFailure:前置条件失败
	st2 := status.New(codes.FailedPrecondition, "precondition failed")
	st2, _ = st2.WithDetails(&errdetails.PreconditionFailure{
		Violations: []*errdetails.PreconditionFailure_Violation{
			{Type: "TOS", Subject: "user.example.com", Description: "terms of service not accepted"},
			{Type: "PAYMENT", Subject: "billing", Description: "no valid payment method"},
		},
	})
	fmt.Printf("precondition error: %v\n", st2.Err())

	// DebugInfo:调试信息(仅用于内部服务,不要暴露给外部)
	st3 := status.New(codes.Internal, "database error")
	st3, _ = st3.WithDetails(&errdetails.DebugInfo{
		StackEntries: []string{
			"main.processOrder() at /app/order.go:42",
			"main.db.Query() at /app/db.go:128",
		},
		Detail: "connection refused to postgres:5432",
	})
	fmt.Printf("debug error: %v\n", st3.Err())

	// LocalizedMessage:多语言错误消息
	st4 := status.New(codes.InvalidArgument, "invalid input")
	st4, _ = st4.WithDetails(&errdetails.LocalizedMessage{
		Locale:  "zh-CN",
		Message: "输入无效,请检查后重试",
	})
	fmt.Printf("localized error: %v\n", st4.Err())
}

四、自定义错误信息

当 errdetails 不满足需求时,可以用 anypb.Any 携带自定义的错误详情。

go
package main

import (
	"fmt"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/anypb"
)

// 自定义错误详情(模拟 protoc 生成的消息)
type BusinessErrorDetail struct {
	Code      string
	Severity  string // "INFO", "WARNING", "ERROR", "FATAL"
	Timestamp int64
	Context   map[string]string
}

func main() {
	// 构造自定义详情
	detail := &BusinessErrorDetail{
		Code:      "INSUFFICIENT_BALANCE",
		Severity:  "ERROR",
		Timestamp: 1700000000,
		Context: map[string]string{
			"user_id":     "user-42",
			"required":    "100.00",
			"actual":      "30.00",
			"currency":    "CNY",
		},
	}

	// 注意:真实项目中 detail 需要是 proto.Message
	// 这里用字符串模拟序列化
	st := status.Newf(codes.FailedPrecondition,
		"insufficient balance: need %.2f, have %.2f", 100.00, 30.00)

	// 实际使用 WithDetails 时需要传 proto.Message
	// st, _ = st.WithDetails(detail) // detail 需实现 proto.Message
	fmt.Printf("business error: %v\n", st.Err())
	fmt.Printf("detail: %+v\n", detail)

	// 用 Any 携带任意 proto 消息的示例
	anyErr := &anypb.Any{
		TypeUrl: "type.googleapis.com/shop.v1.BusinessErrorDetail",
		Value:   []byte("serialized-detail"),
	}
	fmt.Printf("any detail type: %s\n", anyErr.TypeUrl)
}

五、错误传播与客户端处理

go
package main

import (
	"context"
	"fmt"
	"log"
	"time"

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

// 模拟服务端返回 rich error
func serverHandler(ctx context.Context, userID string) (string, error) {
	if userID == "" {
		st := status.New(codes.InvalidArgument, "user_id is required")
		st, _ = st.WithDetails(&errdetails.BadRequest{
			FieldViolations: []*errdetails.BadRequest_FieldViolation{
				{Field: "user_id", Description: "must not be empty"},
			},
		})
		return "", st.Err()
	}
	if userID == "unknown" {
		return "", status.Errorf(codes.NotFound, "user %q not found", userID)
	}
	return "user-data-" + userID, nil
}

// 客户端处理函数:区分可重试和不可重试错误
func clientCall(ctx context.Context, userID string) {
	result, err := serverHandler(ctx, userID)
	if err != nil {
		st, ok := status.FromError(err)
		if !ok {
			log.Printf("non-grpc error: %v", err)
			return
		}

		switch st.Code() {
		case codes.InvalidArgument:
			// 参数错误:不可重试,需修正参数
			fmt.Printf("[client] invalid argument: %s\n", st.Message())
			for _, d := range st.Details() {
				if br, ok := d.(*errdetails.BadRequest); ok {
					for _, v := range br.FieldViolations {
						fmt.Printf("  field %q: %s\n", v.Field, v.Description)
					}
				}
			}

		case codes.NotFound:
			// 资源不存在:不可重试,走降级逻辑
			fmt.Printf("[client] not found: %s (using default)\n", st.Message())

		case codes.Unavailable:
			// 服务不可用:可重试
			fmt.Printf("[client] unavailable: %s (will retry)\n", st.Message())

		case codes.DeadlineExceeded:
			// 超时:可重试
			fmt.Printf("[client] timeout: %s\n", st.Message())

		case codes.Unauthenticated:
			// 未认证:刷新 token 后重试
			fmt.Printf("[client] unauthenticated: %s (refreshing token)\n", st.Message())

		case codes.PermissionDenied:
			// 无权限:不可重试
			fmt.Printf("[client] permission denied: %s\n", st.Message())

		default:
			fmt.Printf("[client] error code=%v: %s\n", st.Code(), st.Message())
		}
		return
	}
	fmt.Printf("[client] success: %s\n", result)
}

func main() {
	ctx := context.Background()

	fmt.Println("--- empty user_id ---")
	clientCall(ctx, "")

	fmt.Println("\n--- unknown user ---")
	clientCall(ctx, "unknown")

	fmt.Println("\n--- valid user ---")
	clientCall(ctx, "user-42")

	_ = time.Second
}

六、rich error 模式实战

下面展示一个服务端如何根据不同业务场景返回丰富的错误信息。

go
package main

import (
	"context"
	"fmt"
	"time"

	"google.golang.org/genproto/googleapis/rpc/errdetails"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/durationpb"
)

// 业务场景:创建订单
type CreateOrderRequest struct {
	UserID    string
	ProductID string
	Quantity  int32
	Address   string
}

func createOrder(ctx context.Context, req *CreateOrderRequest) (string, error) {
	// 场景1:参数校验失败
	var violations []*errdetails.BadRequest_FieldViolation
	if req.UserID == "" {
		violations = append(violations, &errdetails.BadRequest_FieldViolation{
			Field: "user_id", Description: "must not be empty",
		})
	}
	if req.Quantity <= 0 {
		violations = append(violations, &errdetails.BadRequest_FieldViolation{
			Field: "quantity", Description: "must be positive",
		})
	}
	if req.Quantity > 100 {
		violations = append(violations, &errdetails.BadRequest_FieldViolation{
			Field: "quantity", Description: "must not exceed 100 per order",
		})
	}
	if len(violations) > 0 {
		st := status.New(codes.InvalidArgument, "validation failed")
		st, _ = st.WithDetails(&errdetails.BadRequest{FieldViolations: violations})
		return "", st.Err()
	}

	// 场景2:商品不存在
	if req.ProductID == "nonexistent" {
		st := status.New(codes.NotFound, "product not found")
		st, _ = st.WithDetails(&errdetails.ErrorInfo{
			Reason: "PRODUCT_NOT_FOUND",
			Domain: "shop.example.com",
			Metadata: map[string]string{
				"product_id": req.ProductID,
				"suggest":    "check product catalog",
			},
		})
		return "", st.Err()
	}

	// 场景3:库存不足,附带重试建议
	if req.Quantity > 50 {
		st := status.New(codes.ResourceExhausted, "insufficient stock")
		st, _ = st.WithDetails(&errdetails.RetryInfo{
			RetryDelay: durationpb.New(30 * time.Second),
		})
		st, _ = st.WithDetails(&errdetails.ErrorInfo{
			Reason: "LOW_STOCK",
			Metadata: map[string]string{
				"available": "50",
				"requested": fmt.Sprintf("%d", req.Quantity),
			},
		})
		return "", st.Err()
	}

	// 场景4:用户未验证邮箱(前置条件不满足)
	if req.UserID == "unverified" {
		st := status.New(codes.FailedPrecondition, "email not verified")
		st, _ = st.WithDetails(&errdetails.PreconditionFailure{
			Violations: []*errdetails.PreconditionFailure_Violation{
				{
					Type:        "EMAIL_VERIFICATION",
					Subject:     "user.example.com",
					Description: "please verify your email before placing orders",
				},
			},
		})
		return "", st.Err()
	}

	// 场景5:成功
	return fmt.Sprintf("order-%s-%d", req.ProductID, time.Now().Unix()), nil
}

func main() {
	cases := []struct {
		name string
		req  *CreateOrderRequest
	}{
		{"empty user + bad quantity", &CreateOrderRequest{UserID: "", Quantity: -1}},
		{"product not found", &CreateOrderRequest{UserID: "u1", ProductID: "nonexistent", Quantity: 1}},
		{"low stock", &CreateOrderRequest{UserID: "u1", ProductID: "p1", Quantity: 99}},
		{"unverified user", &CreateOrderRequest{UserID: "unverified", ProductID: "p1", Quantity: 1}},
		{"success", &CreateOrderRequest{UserID: "u1", ProductID: "p1", Quantity: 5}},
	}

	for _, c := range cases {
		fmt.Printf("\n=== %s ===\n", c.name)
		orderID, err := createOrder(context.Background(), c.req)
		if err != nil {
			st, _ := status.FromError(err)
			fmt.Printf("code: %v\n", st.Code())
			fmt.Printf("message: %s\n", st.Message())
			for _, d := range st.Details() {
				switch detail := d.(type) {
				case *errdetails.BadRequest:
					for _, v := range detail.FieldViolations {
						fmt.Printf("  BadRequest: field=%s desc=%s\n", v.Field, v.Description)
					}
				case *errdetails.ErrorInfo:
					fmt.Printf("  ErrorInfo: reason=%s metadata=%v\n", detail.Reason, detail.Metadata)
				case *errdetails.RetryInfo:
					fmt.Printf("  RetryInfo: retry_after=%v\n", detail.RetryDelay.AsDuration())
				case *errdetails.PreconditionFailure:
					for _, v := range detail.Violations {
						fmt.Printf("  Precondition: type=%s desc=%s\n", v.Type, v.Description)
					}
				}
			}
		} else {
			fmt.Printf("success: orderID=%s\n", orderID)
		}
	}
}

七、错误映射:HTTP 状态码与 gRPC 状态码

gRPC-Gateway 和 REST 代理需要将 gRPC 状态码映射为 HTTP 状态码。标准映射关系如下:

gRPC 状态码HTTP 状态码HTTP 方法约束
OK200All
Canceled499All
Unknown500All
InvalidArgument400All
DeadlineExceeded504All
NotFound404All
AlreadyExists409POST/PUT
PermissionDenied403All
ResourceExhausted429All
FailedPrecondition400All
Aborted409All
OutOfRange400All
Unimplemented501All
Internal500All
Unavailable503All
DataLoss500All
Unauthenticated401All
go
package main

import (
	"fmt"
	"net/http"

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

// gRPC 状态码 -> HTTP 状态码
func grpcCodeToHTTP(code codes.Code) int {
	switch code {
	case codes.OK:
		return http.StatusOK
	case codes.Canceled:
		return 499 // Client Closed Request
	case codes.Unknown:
		return http.StatusInternalServerError
	case codes.InvalidArgument:
		return http.StatusBadRequest
	case codes.DeadlineExceeded:
		return http.StatusGatewayTimeout
	case codes.NotFound:
		return http.StatusNotFound
	case codes.AlreadyExists:
		return http.StatusConflict
	case codes.PermissionDenied:
		return http.StatusForbidden
	case codes.ResourceExhausted:
		return http.StatusTooManyRequests
	case codes.FailedPrecondition:
		return http.StatusBadRequest
	case codes.Aborted:
		return http.StatusConflict
	case codes.OutOfRange:
		return http.StatusBadRequest
	case codes.Unimplemented:
		return http.StatusNotImplemented
	case codes.Internal:
		return http.StatusInternalServerError
	case codes.Unavailable:
		return http.StatusServiceUnavailable
	case codes.DataLoss:
		return http.StatusInternalServerError
	case codes.Unauthenticated:
		return http.StatusUnauthorized
	default:
		return http.StatusInternalServerError
	}
}

// HTTP 状态码 -> gRPC 状态码(反向映射,用于 REST 客户端调用 gRPC)
func httpToGRPCCode(httpCode int) codes.Code {
	switch httpCode {
	case http.StatusOK:
		return codes.OK
	case http.StatusBadRequest:
		return codes.InvalidArgument
	case http.StatusUnauthorized:
		return codes.Unauthenticated
	case http.StatusForbidden:
		return codes.PermissionDenied
	case http.StatusNotFound:
		return codes.NotFound
	case http.StatusConflict:
		return codes.AlreadyExists
	case http.StatusTooManyRequests:
		return codes.ResourceExhausted
	case http.StatusNotImplemented:
		return codes.Unimplemented
	case http.StatusServiceUnavailable:
		return codes.Unavailable
	case http.StatusGatewayTimeout:
		return codes.DeadlineExceeded
	case http.StatusInternalServerError:
		return codes.Internal
	default:
		return codes.Unknown
	}
}

func main() {
	// 测试正向映射
	testCodes := []codes.Code{
		codes.OK, codes.NotFound, codes.InvalidArgument,
		codes.Unauthenticated, codes.ResourceExhausted, codes.Internal,
	}
	for _, c := range testCodes {
		fmt.Printf("%-20s -> HTTP %d\n", c.String(), grpcCodeToHTTP(c))
	}

	fmt.Println()

	// 测试反向映射
	testHTTP := []int{200, 400, 401, 403, 404, 429, 500, 503}
	for _, h := range testHTTP {
		fmt.Printf("HTTP %-4d -> %s\n", h, httpToGRPCCode(h).String())
	}

	// 演示在 HTTP handler 中包装 gRPC 错误
	err := status.Error(codes.NotFound, "user not found")
	httpCode := grpcCodeToHTTP(status.Code(err))
	fmt.Printf("\nHTTP response: %d %s\n", httpCode, http.StatusText(httpCode))
}

八、错误日志与追踪

错误应该携带足够的上下文信息,便于排查问题,同时不泄露敏感信息。

go
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
)

// 错误日志结构体
type ErrorLog struct {
	Timestamp  string            `json:"timestamp"`
	Method     string            `json:"method"`
	Code       string            `json:"code"`
	Message    string            `json:"message"`
	RequestID  string            `json:"request_id"`
	UserID     string            `json:"user_id,omitempty"`
	StackTrace string            `json:"stack_trace,omitempty"`
	Context    map[string]string `json:"context,omitempty"`
}

func logError(ctx context.Context, method string, err error) {
	st, _ := status.FromError(err)

	errorLog := ErrorLog{
		Timestamp: time.Now().Format(time.RFC3339Nano),
		Method:    method,
		Code:      st.Code().String(),
		Message:   st.Message(),
		Context:   make(map[string]string),
	}

	// 从 context 提取 request-id
	if md, ok := metadata.FromIncomingContext(ctx); ok {
		if v := md.Get("x-request-id"); len(v) > 0 {
			errorLog.RequestID = v[0]
		}
	}

	// 从 context 提取 user-id
	if uid, ok := ctx.Value("userID").(string); ok {
		errorLog.UserID = uid
	}

	// 根据错误码决定日志级别和是否记录堆栈
	switch st.Code() {
	case codes.Internal, codes.Unknown, codes.DataLoss:
		// 严重错误:记录完整堆栈,触发告警
		errorLog.StackTrace = "goroutine 1 [running]:\nmain.something()\n\t/app/main.go:42"
		log.Printf("[ERROR] %+v", errorLog)
		// 实际项目中:发送到 Sentry/Prometheus Alert

	case codes.Unavailable, codes.DeadlineExceeded:
		// 基础设施错误:记录,可能触发告警
		log.Printf("[WARN] %+v", errorLog)

	case codes.InvalidArgument, codes.NotFound, codes.AlreadyExists:
		// 客户端错误:记录为 INFO,不告警
		log.Printf("[INFO] %+v", errorLog)

	default:
		log.Printf("[INFO] %+v", errorLog)
	}
}

func main() {
	ctx := metadata.NewIncomingContext(context.Background(),
		metadata.Pairs("x-request-id", "req-log-001"))
	ctx = context.WithValue(ctx, "userID", "user-42")

	// 模拟不同类型的错误
	errors := []struct {
		method string
		err    error
	}{
		{"/shop.OrderService/GetOrder", status.Error(codes.NotFound, "order 123 not found")},
		{"/shop.OrderService/CreateOrder", status.Error(codes.InvalidArgument, "quantity must be positive")},
		{"/shop.OrderService/Pay", status.Error(codes.Internal, "database connection lost")},
		{"/shop.OrderService/Cancel", status.Error(codes.Unavailable, "payment service unavailable")},
	}

	for _, e := range errors {
		fmt.Printf("\n--- %s ---\n", e.method)
		logError(ctx, e.method, e.err)
	}
}

九、完整示例:带丰富错误信息的用户服务

go
package main

import (
	"context"
	"fmt"
	"log"
	"regexp"
	"strings"
	"time"

	"google.golang.org/genproto/googleapis/rpc/errdetails"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/durationpb"
)

// === 数据模型 ===
type User struct {
	ID    string
	Name  string
	Email string
	Age   int32
	Role  string
}

// === 模拟存储 ===
var userStore = map[string]*User{
	"u1": {ID: "u1", Name: "alice", Email: "alice@example.com", Age: 30, Role: "user"},
	"u2": {ID: "u2", Name: "bob", Email: "bob@example.com", Age: 25, Role: "admin"},
}

// === 请求类型 ===
type CreateUserRequest struct {
	Name  string
	Email string
	Age   int32
}

// === 服务实现 ===
type UserService struct{}

var emailRegex = regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}$`)

func (s *UserService) CreateUser(ctx context.Context, req *CreateUserRequest) (*User, error) {
	// 1. 参数校验
	var violations []*errdetails.BadRequest_FieldViolation
	if len(req.Name) < 2 {
		violations = append(violations, &errdetails.BadRequest_FieldViolation{
			Field: "name", Description: "must be at least 2 characters",
		})
	}
	if !emailRegex.MatchString(req.Email) {
		violations = append(violations, &errdetails.BadRequest_FieldViolation{
			Field: "email", Description: "must be a valid email address",
		})
	}
	if req.Age < 0 || req.Age > 150 {
		violations = append(violations, &errdetails.BadRequest_FieldViolation{
			Field: "age", Description: "must be between 0 and 150",
		})
	}
	if len(violations) > 0 {
		st := status.New(codes.InvalidArgument, "validation failed")
		st, _ = st.WithDetails(&errdetails.BadRequest{FieldViolations: violations})
		return nil, st.Err()
	}

	// 2. 唯一性检查
	for _, existing := range userStore {
		if strings.EqualFold(existing.Email, req.Email) {
			st := status.New(codes.AlreadyExists, "email already registered")
			st, _ = st.WithDetails(&errdetails.ErrorInfo{
				Reason: "EMAIL_DUPLICATE",
				Domain: "user.example.com",
				Metadata: map[string]string{
					"email":    req.Email,
					"existing_user_id": existing.ID,
				},
			})
			return nil, st.Err()
		}
	}

	// 3. 创建用户
	user := &User{
		ID:    fmt.Sprintf("u%d", len(userStore)+1),
		Name:  req.Name,
		Email: req.Email,
		Age:   req.Age,
		Role:  "user",
	}
	userStore[user.ID] = user
	return user, nil
}

func (s *UserService) GetUser(ctx context.Context, userID string) (*User, error) {
	user, ok := userStore[userID]
	if !ok {
		st := status.New(codes.NotFound, "user not found")
		st, _ = st.WithDetails(&errdetails.ErrorInfo{
			Reason: "USER_NOT_FOUND",
			Domain: "user.example.com",
			Metadata: map[string]string{
				"user_id": userID,
				"hint":    "use ListUsers to see all available users",
			},
		})
		return nil, st.Err()
	}
	return user, nil
}

func (s *UserService) DeleteUser(ctx context.Context, userID, requesterRole string) error {
	if requesterRole != "admin" {
		st := status.New(codes.PermissionDenied, "admin role required")
		st, _ = st.WithDetails(&errdetails.ErrorInfo{
			Reason: "INSUFFICIENT_ROLE",
			Metadata: map[string]string{
				"required_role": "admin",
				"actual_role":   requesterRole,
				"user_id":       userID,
			},
		})
		return st.Err()
	}
	if _, ok := userStore[userID]; !ok {
		return status.Errorf(codes.NotFound, "user %s not found", userID)
	}
	delete(userStore, userID)
	return nil
}

// === 客户端错误处理 ===
func printError(method string, err error) {
	st, ok := status.FromError(err)
	if !ok {
		fmt.Printf("[%s] non-grpc error: %v\n", method, err)
		return
	}
	fmt.Printf("[%s] code=%s message=%s\n", method, st.Code(), st.Message())
	for _, d := range st.Details() {
		switch detail := d.(type) {
		case *errdetails.BadRequest:
			for _, v := range detail.FieldViolations {
				fmt.Printf("  -> field %q: %s\n", v.Field, v.Description)
			}
		case *errdetails.ErrorInfo:
			fmt.Printf("  -> reason=%s metadata=%v\n", detail.Reason, detail.Metadata)
		case *errdetails.RetryInfo:
			fmt.Printf("  -> retry after %v\n", detail.RetryDelay.AsDuration())
		}
	}
}

func main() {
	svc := &UserService{}
	ctx := context.Background()

	// 场景1:参数校验失败
	fmt.Println("=== Create: validation failed ===")
	_, err := svc.CreateUser(ctx, &CreateOrderRequestWrapper().Request)
	printError("CreateUser", err)

	// 场景2:邮箱重复
	fmt.Println("\n=== Create: email duplicate ===")
	_, err = svc.CreateUser(ctx, &CreateUserRequest{Name: "alice2", Email: "alice@example.com", Age: 20})
	printError("CreateUser", err)

	// 场景3:成功创建
	fmt.Println("\n=== Create: success ===")
	user, err := svc.CreateUser(ctx, &CreateUserRequest{Name: "charlie", Email: "charlie@example.com", Age: 28})
	if err != nil {
		printError("CreateUser", err)
	} else {
		fmt.Printf("created user: %+v\n", user)
	}

	// 场景4:用户不存在
	fmt.Println("\n=== Get: not found ===")
	_, err = svc.GetUser(ctx, "nonexistent")
	printError("GetUser", err)

	// 场景5:权限不足
	fmt.Println("\n=== Delete: permission denied ===")
	err = svc.DeleteUser(ctx, "u1", "user")
	printError("DeleteUser", err)

	// 场景6:成功删除
	fmt.Println("\n=== Delete: success ===")
	err = svc.DeleteUser(ctx, "u2", "admin")
	if err != nil {
		printError("DeleteUser", err)
	} else {
		fmt.Println("user deleted successfully")
	}

	_ = time.Second
	_ = durationpb.New
	_ = log.Printf
}

// 辅助函数:构造一个校验失败的请求
func CreateOrderRequestWrapper() struct{ Request *CreateUserRequest } {
	return struct{ Request *CreateUserRequest }{
		Request: &CreateUserRequest{Name: "x", Email: "invalid", Age: 200},
	}
}

十、小结

本篇深入 gRPC 的错误处理与状态码体系:

  • 状态码体系:17 个标准状态码,每个都有明确的语义和 HTTP 映射,选择正确状态码是良好 API 设计的基础。
  • status 包status.New/status.Error/status.Errorf 创建错误,status.FromError/status.Code 解析错误。
  • 错误详情(errdetails):通过 WithDetails 携带结构化的错误信息——BadRequest(字段级校验)、ErrorInfo(原因+元数据)、RetryInfo(重试建议)、QuotaFailure(配额超限)、PreconditionFailure(前置条件)等。
  • 自定义错误:用 anypb.Any 携带业务自定义的错误详情 proto 消息。
  • 错误传播:客户端通过 status.FromError 解析,按状态码分类处理(可重试 vs 不可重试 vs 降级)。
  • HTTP 映射:gRPC 状态码与 HTTP 状态码的双向映射表,用于 gRPC-Gateway 等代理层。
  • 错误日志:按状态码分级记录(Internal 严重告警、Unavailable 基础设施告警、InvalidArgument 仅记录),携带 request-id 和上下文。
  • 完整示例:用户服务综合运用了字段校验、唯一性检查、权限控制、资源不存在等场景的 rich error。

下一篇将学习 gRPC-Gateway,将 gRPC 服务自动转换为 RESTful API。