如何在Golang中实现RPC超时控制_Golang RPC请求超时处理方法

net/rpc 默认不支

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

Go net/rpc 默认不支持超时,必须自己封装

Go 标准库 net/rpcClient.CallClient.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
}

}

这个模式安全、简洁,且兼容所有 net/rpc 传输方式(TCP、Unix socket、甚至自定义 io.ReadWriteCloser)。

  • done channel 容量为 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),否则单次超时可能拖垮整个连接池。