如何在Golang中处理RPC错误返回_RPC错误处理技巧

Go net/rpc错误需同时检查两层:client.Call返回的err表示网络/协议失败,call.Error非nil才表示服务端业务错误;方法签名必须为func(T,S)error且用指针,panic会导致连接中断,建议用jsonrpc提升错误可读性。

Go net/rpc 的错误返回机制不走 error 返回值

Go 标准库的 net/rpc(尤其是基于 HTTP 或 TCP 的服务端)不会把方法执行中的 panic 或 error 作为函数返回值抛给客户端。它只通过响应体里的 ErrorResponse 字段传递错误,且**仅当方法签名形如 func(*Args, *Reply) error 且返回非 nil error 时**,才会被序列化为 RPC 错误。否则,panic 会导致连接中断,客户端收到 rpc: server failed to decode args: EOF 这类底层错误,无法区分业务逻辑失败和网络异常。

  • 必须确保 RPC 方法签名严格符合 func(*T, *S) error 形式,*T*S 都要是指针类型
  • 不要在方法内部直接 log.Fatalos.Exit,这会终止整个 server
  • 如果想透传底层 error(比如数据库超时),建议包装成自定义 error 并实现 Error() 方法,便于客户端解析

客户端如何可靠判断 RPC 调用是否失败

调用 client.Callclient.Go 后,不能只检查返回的 err 是否为 nil —— 这个 err 仅代表“调用发起失败”(如连接 refused、序列化失败),不代表业务逻辑执行失败。真正的业务错误藏在 reply 对应结构体的某个字段里,或者更常见的是:只有当 call.Error != nil 时,才表示服务端明确返回了 error。

  • call.Error 是服务端方法返回的 error 值反序列化结果,可能为 "invalid user id" 这类字符串,也可能是 JSON 编码后的结构体(取决于编码器)
  • 标准 gob 编码下,call.Error 类型是 *rpc.Error,其 Err 字段是 string;若用 jsonrpc,则可能是 map 或自定义结构
  • 务必同时检查:if err != nil { /* 网络/协议层失败 */ }if call.Error != nil { /* 业务逻辑失败 */ }

jsonrpc 替代默认 gob 提升错误可读性

默认 gob 编码对 error 的序列化是二进制且不可读的,调试困难;jsonrpc 把 error 映射为 JSON-RPC 2.0 规范的 error 对象(含 codemessagedata),更适合跨语言或前端联调场景。

package main

import ( "net/rpc/jsonrpc" "net/http" )

func main() { http.Handle("/rpc", jsonrpc.NewHTTPServer(&MyService{})) http.ListenAndServe(":8080", nil) }

type MyService struct{}

func (s MyService) Divide(args DivArgs, reply *DivReply) error { if args.B == 0 { return &jsonrpc.RPCError{ Code: -32602, Message: "division by zero", Data: map[string]string{"field": "b"}, } } reply.Result = args.A / args.B return nil }

  • jsonrpc.RPCError 是标准兼容结构,客户端(如 curl 或 JS fetch)能直接识别 codemessage
  • 注意:服务端需用 jsonrpc.NewHTTPServer,客户端需用 jsonrpc.DialHTTP 或手动构造 JSON-RPC 请求体
  • 避免在 Data 中塞敏感信息或大对象,JSON 序列化有开销

服务端 panic 导致连接重置的兜底处理

即使写了 defer/recover,RPC handler 内部未捕获的 panic 仍会让 net/rpc 关闭连接,客户端看到类似 read tcp ...: i/o timeoutbroken pipe。这不是 bug,是设计使然 —— RPC 框架不负责 recover。

  • 最稳妥的方式是在每个注册的方法里加统一 recover:
func (s *MyService) SafeMethod(args *Args, reply *Reply) error {
    defer func() {
        if r := recover(); r != nil {
            log.Printf("panic in SafeMethod: %v", r)
            // 返回一个明确的业务错误,而非让连接断开
        }
    }()
    // 实际逻辑...
    return nil
}
  • 也可以用中间件思路:封装一个 wrapHandler 函数,自动注入 defer-recover,再注册到 server
  • 但要注意:recover 后不能继续写 response,因为连接可能已半关闭;只能返回 error,由 RPC 框架统一处理

实际项目中,最容易被忽略的是「同时检查两层 error」—— 网络层 err 和业务层 call.Error。漏掉任一,都会让错误流丢失上下文,排查时只能靠日志猜。