在Golang的RPC开发中,错误返回的处理需要兼顾服务端的错误定义、传输过程的序列化以及调用端的错误解析,不同RPC框架的实现逻辑存在一定差异,但核心目标都是让错误信息准确、完整地传递到调用方。

Golang标准库net/rpc的错误处理方式
标准库的net/rpc要求服务方法必须遵循固定的签名格式,其中最后一个返回值必须是error类型,这是错误返回的基础规则。
服务端错误定义与返回
服务端在方法实现中可以直接返回自定义错误,标准库会将error序列化后传递给调用端。示例如下:
package main
import (
"errors"
"net"
"net/rpc"
)
// 定义参数结构体
type Args struct {
A int
B int
}
// 定义回复结构体
type Reply struct {
Result int
}
// 算术服务结构体
type Arith int
// 除法方法,返回错误
func (t *Arith) Divide(args *Args, reply *Reply) error {
if args.B == 0 {
// 返回自定义错误
return errors.New("除数不能为0")
}
reply.Result = args.A / args.B
return nil
}
func main() {
arith := new(Arith)
rpc.Register(arith)
listener, _ := net.Listen("tcp", ":1234")
rpc.Accept(listener)
}
调用端错误解析
调用端调用RPC方法后,会接收到返回的error,直接判断即可:
package main
import (
"fmt"
"net/rpc"
)
type Args struct {
A int
B int
}
type Reply struct {
Result int
}
func main() {
client, _ := rpc.Dial("tcp", "127.0.0.1:1234")
defer client.Close()
args := &Args{A: 10, B: 0}
reply := &Reply{}
// 调用RPC方法,接收错误
err := client.Call("Arith.Divide", args, reply)
if err != nil {
fmt.Println("RPC调用失败:", err)
return
}
fmt.Println("计算结果:", reply.Result)
}
这种方式的局限性是只能传递字符串形式的错误信息,无法携带错误码等结构化数据。
grpc框架下的RPC错误处理
grpc是Golang中更常用的RPC框架,它内置了更完善的错误传递机制,支持携带错误码、错误信息以及附加的元数据。
服务端返回结构化错误
grpc提供了status包和codes包来定义结构化错误,示例如下:
package main
import (
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"net"
)
// 定义请求结构体
type DivideRequest struct {
A int32
B int32
}
// 定义响应结构体
type DivideResponse struct {
Result int32
}
// 实现算术服务接口
type ArithServer struct{}
func (s *ArithServer) Divide(ctx context.Context, req *DivideRequest) (*DivideResponse, error) {
if req.B == 0 {
// 返回带错误码的结构化错误
return nil, status.Errorf(codes.InvalidArgument, "除数不能为0,当前除数为%d", req.B)
}
return &DivideResponse{Result: req.A / req.B}, nil
}
func main() {
server := grpc.NewServer()
// 注册服务逻辑省略
listener, _ := net.Listen("tcp", ":50051")
server.Serve(listener)
}
调用端解析grpc错误
调用端可以通过status包解析返回的错误,获取错误码和详细信息:
package main
import (
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type DivideRequest struct {
A int32
B int32
}
type DivideResponse struct {
Result int32
}
func main() {
conn, _ := grpc.Dial("127.0.0.1:50051", grpc.WithInsecure())
defer conn.Close()
// 调用服务逻辑省略
req := &DivideRequest{A: 10, B: 0}
// 假设已经获取到client并调用Divide方法
// resp, err := client.Divide(context.Background(), req)
// 模拟错误返回
err := status.Errorf(codes.InvalidArgument, "除数不能为0,当前除数为0")
if err != nil {
st, ok := status.FromError(err)
if ok {
fmt.Printf("错误码: %v, 错误信息: %sn", st.Code(), st.Message())
if st.Code() == codes.InvalidArgument {
fmt.Println("参数错误,请检查输入")
}
} else {
fmt.Println("非grpc错误:", err)
}
return
}
// fmt.Println("计算结果:", resp.Result)
}
RPC错误处理的最佳实践
- 尽量使用结构化的错误定义,携带错误码、错误描述等必要信息,方便调用端做差异化的错误处理。
- 避免在错误信息中暴露敏感的服务内部信息,比如数据库地址、内部逻辑细节等。
- 对于可重试的错误(比如网络超时)和不可重试的错误(比如参数错误)做明确区分,方便调用端做重试逻辑。
- 统一项目中RPC错误的定义规范,避免不同服务返回的错误格式不一致增加调用端的处理成本。
常见错误处理误区
很多开发者会忽略错误类型的判断,直接把RPC返回的错误当成普通字符串错误处理,导致无法正确解析grpc等框架返回的结构化错误。另外,部分开发者会在服务端返回panic而不是error,这会导致RPC框架无法正确捕获错误,甚至可能让服务直接崩溃,这是需要避免的。
需要注意的是,RPC调用过程中除了业务错误,还可能出现网络错误、超时错误等传输层错误,这些错误也需要调用端做统一的处理,不能只关注服务端返回的业务错误。