net/rpc 默认不支持超时,必须用 context.WithTimeout + goroutine 封装 Call 实现安全超时;jsonrpc.Client 同样适用该方案,其仅编码不同,无内置超时能力。
Go 标准库 net/rpc 的 Client.Call 和 Client.Go 都是同步阻塞调用,**没有内置 timeout 参数**。一旦后端卡住、网络丢包或服务未响应,客户端会无限等待,直到 TCP 层最终断连(通常要几分钟)。这不是业务能接受的。
解决思路只有一条:用 context.Context 包裹 RPC 调用,靠 goroutine + channel 实现带超时的异步等待。
rpc.Client 设置全局超时http.Transport(那是 net/rpc/jsonrpc 或自定义 HTTP 传输时才相关)Call 单独控制超时核心是启动一个 goroutine 执行 client.Call,主协程通过 select 等待结果或 context 超时。注意必须确保 goroutine 在超时后能退出(虽然 Call 本身不会中断,但后续不再读取 channel 即可)。
func callWithTimeout(client *rpc.Client, method 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(method, args, reply)
}()
select {
case err := <-done:
return err
case <-ctx.Done():
return ctx.Err() // 返回 context.DeadlineExceeded
}}
这个模式安全、简洁,且兼容所有 net/rpc 传输方式(TCP、Unix socket、甚至

自定义 io.ReadWriteCloser)。
done channel 容量为 1,避免 goroutine 泄漏ctx.WithCancel() 手动取消 RPC(底层不支持中断)ctx.Err() 是 context.DeadlineExceeded,可直接判断有人以为 net/rpc/jsonrpc.NewClient 是“高级版”,其实它只是把 Go 的 gob 编码换成 JSON,并未增加超时能力。它的 Call 方法仍是阻塞的。
所以对 jsonrpc.Client 同样要用上面的 context 封装方案。唯一区别是初始化方式:
conn, _ := net.Dial("tcp", "localhost:8080")
client := jsonrpc.NewClient(conn)
// 后续仍需 callWithTimeout(client, ...)如果用 jsonrpc.NewClientCodec 自定义编解码器,只要底层连接没变,超时逻辑也不变。
错误写法示例:
select {
case err := <-done:
return err
case <-time.After(timeout): // ❌ 错误!每次调用都新建 Timer,不复用会泄漏
return fmt.Errorf("timeout")
}time.After 内部使用未导出的 timer,无法手动停止;高频调用会导致大量 goroutine 等待到期。正确做法永远是 context.WithTimeout,它复用 runtime timer,且 cancel() 能及时清理。
另外注意:RPC 服务端本身也应设好读写 deadline(比如 conn.SetDeadline),否则单次超时可能拖垮整个连接池。