Go net/rpc 错误返回机制不透明,因gob不支持error接口序列化,导致客户端无法获取真实错误信息;应改用jsonrpc并定义结构化错误类型RPCError,将错误作为响应体字段显式传递,同时全局拦截panic并转换为RPCError。
error 会被序列化丢弃Go 标准库 net/rpc 默认使用 gob 编码,而 error 接口本身无法被直接序列化。服务端返回的 error 在客户端收到时往往变成 nil 或泛化为 "rpc: service/method returned error: …" 这类无意义字符串,真实错误信息丢失。
根本原因:gob 不支持接口类型(如 error)的跨进程传输,除非你显式实现 gob.GobEncoder/GobDecoder —— 但标准 errors.New 和 fmt.Errorf 返回的错误都不满足。
return nil, fmt.Errorf("user not found: %d", id),客户端拿到的 err 可能是 nil 或一个空错误*net/rpc.ServerError,其 Error() 方法只返回固定前缀 + 字符串,原始堆栈、类型、字段全丢jsonrpc 替代 gob 并封装结构化错误响应最稳妥的方案是放弃 net/rpc 默认的 gob,改用 net/rpc/jsonrpc,并约定统一的错误响应格式。JSON 能自然序列化 map、struct,便于携带错误码、消息、详情字段。
关键不是换编码,而是把错误「作为返回值的一部分」显式设计,而非依赖 RPC 框架的 error 参数。
(Result, *RPCError) 结构体,其中 RPCError 是你定义的可序列化错误类型call.Error,而是检查返回值中的 *RPCError 字段jsonrpc 中混用 gob 注册逻辑(比如误调 rpc.Register 后又用 jsonrpc.ServeConn)type RPCError struct {
Code int `json:"code"`
Message string `json:"message"`
Details string `json:"details,omitempty"`
}
type GetUserResponse struct {
User *User `json:"user"`
Error *RPCError `json:"error"`
}
func (s *UserService) GetUser(r *GetUserRequest, resp *GetUserResponse) error {
u, err := s.db.FindUser(r.ID)
if err != nil {
*resp = GetUserResponse{
Error: &RPCError{
Code: 404,
Message: "user not found",
Details: err.Error(),
},
}
return nil // 注意:这里 return nil,错误走 resp.Error
}
resp.User = u
return nil
}
rpc.Client.Call 的 err 只反映传输/协议层失败,不是业务错误很多人误以为 client.Call(..., &reply, &err) 中的 err 是服务端抛出的业务错误,其实它只表示:连接断开、超时、编码失败、服务端 panic、方法不存在等底层问题。只要请求发出去且收到响应,这个 err 就是 nil —— 即使服务端逻辑返回了 fmt.Errorf("invalid input")。
err != nil → 网络失败、服务宕机、序列化异常、服务端没启动监听err == nil → 请求已送达且响应已解析,但业务是否成功需检查 reply 内容err 是 "rpc: call failed: EOF" 或类似,不是 panic 信息本身recover 统一转成结构化错误RPC 服务端方法里一旦 panic,整个连接可能中断,jsonrpc 会返回模糊的 "EOF",gob 则直接崩溃。必须主动拦截 panic,并转换为客户端可识别的 RPCError。
推荐在 handler 外层加 recover,而不是每个方法都写 defer —— 可以用中间件模式包装注册的服务对象。
if err != nil { panic(err) }
log.Fatal 或 os.Exit 杀掉整个 serverRPCError{Code: 500, Message: "internal error"} 返回func (s *UserService) safeCall(fn func() error) error {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in RPC method: %+v", r)
// 此处可触发告警、上报 Sentry 等
}
}()
return fn()
}
func (s *UserService) GetUser(r *GetUserRequest, resp *GetUserResponse) error {
return s.safeCall(func() error {
u, err := s.db.FindUser(r.ID)
if err != nil {
*resp = GetUserResponse{
Error: &RPCError{Code: 404, Message: "user not found"},
}
return nil
}
resp.User = u
return nil
})
}
RPC 错误处理的核心不是“怎么捕获 err”,而是“怎么让错误可读、可分类、可追踪”。结构化响应体 + 显式错误字段