net/rpc 默认不支持超时,必须用 context.WithTimeout + goroutine 封装 Call 实现安全超时;jsonrpc.Client 同样适用该方案,其仅编码不同,无内置超时能力。

Go net/rpc 默认不支持超时,必须自己封装
Go 标准库 net/rpc 的 Client.Call 和 Client.Go 都是同步阻塞调用,**没有内置 timeout 参数**。一旦后端卡住、网络丢包或服务未响应,客户端会无限等待,直到 TCP 层最终断连(通常要几分钟)。这不是业务能接受的。
解决思路只有一条:用 context.Context 包裹 RPC 调用,靠 goroutine + channel 实现带超时的异步等待。
- 不能直接给
rpc.Client设置全局超时 - 不能靠修改
http.Transport(那是net/rpc/jsonrpc或自定义 HTTP 传输时才相关) - 必须对每次
Call单独控制超时
用 context.WithTimeout + goroutine 实现安全超时
核心是启动一个 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
}}
立即学习“go语言免费学习笔记(深入)”;
这个模式安全、简洁,且兼容所有 net/rpc 传输方式(TCP、Unix socket、甚至自定义 io.ReadWriteCloser)。
-
donechannel 容量为 1,避免 goroutine 泄漏 - 不用
ctx.WithCancel()手动取消 RPC(底层不支持中断) - 超时后
ctx.Err()是context.DeadlineExceeded,可直接判断
jsonrpc.Client 也一样,别被名字误导
有人以为 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 自定义编解码器,只要底层连接没变,超时逻辑也不变。
不要用 time.After 或 time.Timer 直接 select —— 有泄漏风险
错误写法示例:
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),否则单次超时可能拖垮整个连接池。










