You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

gRPC调用返回的error无法转换为自定义Hater接口的问题

gRPC自定义错误接口跨服务转换问题解决方案

问题根源

gRPC 不会直接序列化传输服务器端返回的原始 error 对象,而是将其转换为符合 gRPC 规范的 status.Status 结构,仅传递错误码、消息及可选详情。客户端接收到的是 gRPC 封装后的错误类型(通常为 *status.Status),而非服务器端的 hater.CustomError 实例,因此直接类型断言 err.(hater.Hater) 必然失败。


解决方案1:通过gRPC错误详情(Details)传递自定义错误

该方案符合gRPC规范,支持传递结构化错误信息,步骤如下:

1. 修改Protobuf定义

新增自定义错误消息类型:

syntax = "proto3";

package errortest;

option go_package = "./errortest";

message EmptyMsg {}
message StatusMsg {}

// 自定义错误的结构化消息
message CustomErrorMsg {
  string message = 1;
}

2. 服务器端改造

将自定义错误编码为Protobuf消息,添加到gRPC错误详情中返回:

package main

import (
    "context"
    "fmt"
    "log"
    "net"

    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
    "google.golang.org/protobuf/types/known/anypb"

    pb "main/errortest"
    "main/hater"
)

type ErrorTestServer struct {
    pb.UnimplementedErrorTestServer
}

func (s ErrorTestServer) ErrorTest(ctx context.Context, _ *pb.EmptyMsg) (*pb.StatusMsg, error) {
    customErr := NewCustomError()
    // 转换为Protobuf消息
    errMsg := &pb.CustomErrorMsg{Message: customErr.Message}
    anyErr, _ := anypb.New(errMsg)
    // 包装为带详情的gRPC错误
    st := status.New(codes.Internal, customErr.Error())
    st, _ = st.WithDetails(anyErr)
    return &pb.StatusMsg{}, st.Err()
}

func NewCustomError() hater.CustomError {
    return hater.CustomError{
        Message: "Greetings from Bali!",
    }
}

func main() {
    lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", 9090))
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    grpcServer := grpc.NewServer()
    pb.RegisterErrorTestServer(grpcServer, ErrorTestServer{})

    if err := grpcServer.Serve(lis); err != nil {
        fmt.Print("failed to start server")
    }
}

3. 客户端改造

解析gRPC错误的详情,重构自定义错误并转换为Hater接口:

package main

import (
    "context"
    "fmt"
    pb "main/errortest"
    "main/hater"
    "reflect"

    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
    "google.golang.org/grpc/status"
)

func main() {
    conn, err := grpc.Dial("localhost:9090", grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        return
    }
    defer conn.Close()

    client := pb.NewErrorTestClient(conn)

    _, err = client.ErrorTest(context.Background(), &pb.EmptyMsg{})
    if err != nil {
        fmt.Println(reflect.TypeOf(err))
        // 转换为gRPC status对象
        st, ok := status.FromError(err)
        if ok {
            // 遍历错误详情
            for _, detail := range st.Details() {
                switch msg := detail.(type) {
                case *pb.CustomErrorMsg:
                    // 重构CustomError实例并断言为Hater
                    customErr := hater.CustomError{Message: msg.Message}
                    if h, ok := customErr.(hater.Hater); ok {
                        h.Show()
                    }
                }
            }
        }
    }
}

解决方案2:通过gRPC元数据(Metadata)传递

适合仅需传递简单字符串信息的场景,实现更简洁:

1. 服务器端改造

将错误信息写入gRPC响应元数据:

package main

import (
    "context"
    "fmt"
    "log"
    "net"

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

    pb "main/errortest"
    "main/hater"
)

type ErrorTestServer struct {
    pb.UnimplementedErrorTestServer
}

func (s ErrorTestServer) ErrorTest(ctx context.Context, _ *pb.EmptyMsg) (*pb.StatusMsg, error) {
    customErr := NewCustomError()
    // 设置响应元数据
    md := metadata.Pairs("custom-error-message", customErr.Message)
    grpc.SetTrailer(ctx, md)
    return &pb.StatusMsg{}, status.Error(codes.Internal, customErr.Error())
}

func NewCustomError() hater.CustomError {
    return hater.CustomError{
        Message: "Greetings from Bali!",
    }
}

func main() {
    lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", 9090))
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    grpcServer := grpc.NewServer()
    pb.RegisterErrorTestServer(grpcServer, ErrorTestServer{})

    if err := grpcServer.Serve(lis); err != nil {
        fmt.Print("failed to start server")
    }
}

2. 客户端改造

提取元数据中的错误信息,重构自定义错误:

package main

import (
    "context"
    "fmt"
    pb "main/errortest"
    "main/hater"
    "reflect"

    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
    "google.golang.org/grpc/metadata"
)

func main() {
    conn, err := grpc.Dial("localhost:9090", grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        return
    }
    defer conn.Close()

    client := pb.NewErrorTestClient(conn)

    var trailer metadata.MD
    _, err = client.ErrorTest(
        context.Background(),
        &pb.EmptyMsg{},
        grpc.Trailer(&trailer),
    )
    if err != nil {
        fmt.Println(reflect.TypeOf(err))
        // 提取元数据中的错误信息
        if msgList := trailer.Get("custom-error-message"); len(msgList) > 0 {
            customErr := hater.CustomError{Message: msgList[0]}
            if h, ok := customErr.(hater.Hater); ok {
                h.Show()
            }
        }
    }
}

方案对比

  • 错误详情方案:符合gRPC规范,支持复杂结构化错误,扩展性强,推荐用于需要传递多字段错误信息的场景。
  • 元数据方案:实现简单,仅支持字符串键值对,适合快速传递少量错误信息的场景。

内容的提问来源于stack exchange,提问作者Bob

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 19:33:13