Golang中如何处理gRPC服务的错误

Golang中如何处理gRPC服务的错误 假设我们有几个用Go编写的gRPC服务,例如:

服务1

func (s *srv1) Foo(req Request, rsp *Response) error {
    // 处理逻辑但发现某些错误
    return err
}

服务2

func (s *srv2) Bar(req Request, rsp *Response) error {
    // 处理逻辑但发现某些错误
    rsp.Code = Err_Code
    rsp.Msg = err.Error()
    return nil
}

服务3

func (s *srv3) Biz(req Request, rsp *Response) error {
    // 处理逻辑且未发现错误
    return nil
}

这里有一个Java客户端调用我们的Go服务:

try {
    srv1.Foo()
    srv2.Bar()
    srv3.Biz()
} catch (Exception e) {
   // 处理异常
}

在我们的场景中,如果srv1返回错误,将会捕获到异常,导致srv2srv3不会被调用。所以我的问题是:我们的服务应该返回错误导致客户端终止,还是应该总是返回nil但提供rsp.Codersp.Msg,这样即使srv1失败,客户端也能调用srv2srv3?或者服务器和客户端是否有更好的错误处理方式?

抱歉我的英语不好,感谢帮助!微笑


更多关于Golang中如何处理gRPC服务的错误的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复

感谢您的建议,我们团队目前选择始终返回nil并使用代码+消息的组合方式。也许将来能找到更好的解决方案…

// 示例代码
func example() error {
    return nil
}

更多关于Golang中如何处理gRPC服务的错误的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


首先,这与Go语言无关,而是与gRPC相关,因此我认为这里不是讨论它的最佳场所。不过,我还是想发表一下我的看法:

root666: 在我们的例子中,如果srv1返回错误,将会捕获到一个异常,导致srv2srv3不会被调用。

这是因为你将所有内容都放在同一个try块中,这是没有必要的。如果你认为在一个服务失败后可以继续调用下一个服务,那么应该分别处理每一个服务。问题不在于“使用rsp.Codersp.Msg而不是……让客户端能够在srv1失败后调用srv2srv3”,而是你如何处理错误以支持后续调用:如果你实际上从srv1获得了一个Err_Code(且错误为nil),你会如何处理?是否会继续调用其他服务?这取决于你的应用程序逻辑。

另一方面,如果你总是返回nil,那么让函数返回错误又有什么意义呢?

值得考虑的是,我会从服务中返回错误,然后在客户端适当地处理它们。

在gRPC服务中,错误处理应该遵循gRPC的标准错误模型。推荐的做法是使用gRPC状态码和错误信息,而不是在响应体中自定义错误字段。

对于Go gRPC服务,应该返回适当的gRPC错误状态:

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

func (s *srv1) Foo(ctx context.Context, req *Request) (*Response, error) {
    // 处理逻辑但发现某些错误
    if err != nil {
        return nil, status.Error(codes.InvalidArgument, "invalid request parameters")
    }
    return &Response{Data: "success"}, nil
}

func (s *srv2) Bar(ctx context.Context, req *Request) (*Response, error) {
    // 处理逻辑但发现某些错误
    if err != nil {
        return nil, status.Error(codes.Internal, "internal server error")
    }
    return &Response{Data: "success"}, nil
}

func (s *srv3) Biz(ctx context.Context, req *Request) (*Response, error) {
    // 处理逻辑且未发现错误
    return &Response{Data: "success"}, nil
}

在Java客户端中,可以这样处理:

try {
    srv1.Foo();
} catch (StatusRuntimeException e) {
    Status status = e.getStatus();
    // 处理srv1错误,但继续执行其他服务调用
    System.out.println("srv1 failed: " + status.getCode() + " - " + status.getDescription());
}

try {
    srv2.Bar();
} catch (StatusRuntimeException e) {
    Status status = e.getStatus();
    // 处理srv2错误
    System.out.println("srv2 failed: " + status.getCode() + " - " + status.getDescription());
}

try {
    srv3.Biz();
} catch (StatusRuntimeException e) {
    Status status = e.getStatus();
    // 处理srv3错误
    System.out.println("srv3 failed: " + status.getCode() + " - " + status.getDescription());
}

如果需要更细粒度的错误信息,可以在响应体中包含错误详情:

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

func (s *srv1) Foo(ctx context.Context, req *Request) (*Response, error) {
    if err != nil {
        st := status.New(codes.InvalidArgument, "invalid request")
        ds, _ := st.WithDetails(&errdetails.BadRequest{
            FieldViolations: []*errdetails.BadRequest_FieldViolation{
                {
                    Field:       "field_name",
                    Description: err.Error(),
                },
            },
        })
        return nil, ds.Err()
    }
    return &Response{Data: "success"}, nil
}

这种方法的优势:

  1. 符合gRPC标准错误处理机制
  2. 客户端可以明确区分成功和失败情况
  3. 支持丰富的错误详情和状态码
  4. 跨语言兼容性好

如果希望某个服务失败不影响其他服务的调用,应该在客户端为每个服务调用分别进行错误处理,而不是在一个try-catch块中包装所有调用。

回到顶部