Go net/rpc 默认不支持超时,需用 context.WithTimeout + goroutine + select 手动封装 Call 方法;超时后必须关闭连接避免复用失败,长期建议迁移到 gRPC 或 jsonrpc2。

Go net/rpc 默认不支持超时,必须手动封装
Go 标准库 net/rpc 的 Client.Call 和 Client.Go 方法本身**没有超时参数**,底层使用阻塞式 conn.Read,一旦网络卡住或服务端 hang 住,调用会无限等待。这不是 bug,是设计如此——它把超时逻辑交由上层控制。
常见错误现象:Call 卡死数分钟才返回,日志无报错,监控看不到超时指标,下游服务因等待而线程耗尽。
- 不能直接对
rpc.Client设置全局超时 - 不能靠
http.Transport那类配置生效(net/rpc不走 HTTP) - 必须基于
context.Context+ 并发协程 +select手动实现非阻塞等待
用 context.WithTimeout 包裹 Call 调用(推荐方案)
核心思路:启动一个 goroutine 执行 client.Call,主 goroutine 用 select 等待完成或超时。这是最轻量、兼容所有 net/rpc 传输层(TCP、Unix socket)的做法。
注意:必须确保 client 是并发安全的(标准 *rpc.Client 是的),且不要在超时后继续复用已中断的连接(见下条)。
立即学习“go语言免费学习笔记(深入)”;
func callWithTimeout(client *rpc.Client, serviceMethod string, args interface{}, reply interface{}, timeout time.Duration) error {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
done := make(chan error, 1)
go func() {
done <- client.Call(serviceMethod, args, reply)
}()
select {
case err := <-done:
return err
case <-ctx.Done():
return ctx.Err() // 返回 context.DeadlineExceeded
}}
-
timeout建议设为略大于服务端 P99 响应时间,避免误杀正常请求 - 超时后
ctx.Err()是context.DeadlineExceeded,可据此区分是超时还是服务端真实错误 - 该模式不关闭底层连接,但后续调用可能失败;生产环境建议超时后主动
client.Close()并重建
使用 jsonrpc2 或 gRPC 替代标准 net/rpc(长期演进方向)
标准 net/rpc 已多年未更新,缺乏原生上下文、流控、超时、加密等能力。若项目允许升级协议,golang.org/x/net/rpc/jsonrpc(jsonrpc2)和 gRPC-Go 是更现代的选择。
jsonrpc2 示例:它仍基于 net/rpc,但封装了 io.ReadWriter,可配合带超时的 net.Conn 使用:
conn, err := net.DialTimeout("tcp", "localhost:8080", 5*time.Second)
if err != nil {
return err
}
defer conn.Close()
// 设置读写超时(影响单次 Call)
conn.SetDeadline(time.Now().Add(10 * time.Second))
client := jsonrpc.NewClient(conn)-
jsonrpc.NewClient本身不处理超时,但底层conn的SetDeadline会作用于每次Read/Write -
gRPC-Go原生支持context.WithTimeout,且超时会自动取消 stream、释放资源,无需手动 goroutine 封装 - 替换成本较高:需重写服务端、IDL 定义、序列化逻辑,适合新项目或重构阶段
超时后连接状态容易被忽略,导致后续调用失败
这是最容易踩的坑:超时发生后,底层 TCP 连接可能处于半开状态(例如服务端已断连但客户端还没收到 FIN),此时复用该 *rpc.Client 实例发起下一次 Call,大概率返回 io.EOF 或 connection reset by peer,而非你期望的超时错误。
解决方案不是“重试”,而是“隔离”:
- 每个 RPC 调用使用独立的
*rpc.Client(适合低频调用) - 超时后立即
client.Close(),并从连接池中剔除该实例 - 使用连接池时(如
github.com/hashicorp/yamux),需监听client.Closed事件清理连接 - 永远不要假设一次超时不影响后续调用——连接状态不可信
复杂点在于:超时控制和连接生命周期管理必须耦合设计,单独做超时封装而不处理连接,等于只修了半扇窗。










