
本文揭示 go 标准库 net/rpc 中一个常见却易被忽视的问题:客户端在服务端宕机后仍持续输出“正确”结果,并非因内置缓存,而是因忽略 rpc.call.error 导致零值误判为有效响应。
本文揭示 go 标准库 net/rpc 中一个常见却易被忽视的问题:客户端在服务端宕机后仍持续输出“正确”结果,并非因内置缓存,而是因忽略 rpc.call.error 导致零值误判为有效响应。
Go 的 net/rpc 包本身不提供任何响应缓存机制——既无客户端本地缓存,也无代理层或 HTTP 层的隐式缓存。你观察到的“服务已下线却仍有返回值”现象,本质是错误未被检测 + 零值静默填充导致的假象。
回顾你的客户端代码关键片段:
divCall = client.Go("Fibonacci.Calculate", args, &reply, nil)
go func() {
replyCall := <-divCall.Done
r := replyCall.Reply.(*int) // ⚠️ 危险!未检查 replyCall.Error
log.Debug("reply: ", r)
}()当 RPC 连接中断(如服务端进程退出、VM 关机、网络断开)后,replyCall.Error 将被设为非 nil(例如 rpc: connection is shut down 或 i/o timeout),但 reply 变量(类型为 *int)因 Go 的零值初始化规则,其指向的 int 值仍为 0。若你不检查 replyCall.Error,就直接解引用 *int 并打印,就会误以为 fibonacci(0) == 0 是有效计算结果——而实际上请求根本未到达服务端。
✅ 正确做法:始终验证 replyCall.Error
go func() {
replyCall := <-divCall.Done
if replyCall.Error != nil {
log.Error("RPC call failed:", replyCall.Error)
return
}
r := replyCall.Reply.(*int)
log.Debug("reply: ", *r) // 注意解引用以打印实际值
}()此外,还需注意以下几点以构建健壮的 RPC 客户端:
- 连接复用 ≠ 自动重连:rpc.DialHTTP 建立的是持久连接,但连接断开后 client.Go() 不会自动重连,后续调用将立即失败(Error 非 nil)。生产环境应实现连接健康检查与按需重拨逻辑。
- 平台差异非缓存所致:你在 Linux 和 Windows 上观察到不同行为(Linux 返回 0,Windows 似有“缓存”),实为底层 TCP 连接状态探测时机、超时策略及 Go runtime 网络栈实现细节差异所致,并非 RPC 层设计缓存。
- 避免并发写共享变量:示例中 &reply 被多个 goroutine 共享且未加锁,存在竞态风险。推荐为每次调用分配独立 reply 变量:
go func(args *services.FibonacciArgs) {
var reply int
divCall := client.Go("Fibonacci.Calculate", args, &reply, nil)
replyCall := <-divCall.Done
if replyCall.Error != nil {
log.Error("RPC failed for n=", args.N, ": ", replyCall.Error)
return
}
log.Debug("fib(", args.N, ") =", reply)
}(args) // 闭包捕获 args,隔离 reply 作用域总结:Go RPC 没有魔法缓存——只有严谨的错误处理才能暴露真实状态。永远把 replyCall.Error != nil 作为响应可信的前提,而非依赖返回值是否“看起来合理”。这是分布式系统容错的第一道防线。

















