rpc.client 默认不支持并发请求,因其依赖顺序读取响应匹配请求,无请求id机制;需自定义seq字段、原子计数器、pending map及专用响应读取goroutine实现安全并发。

为什么 rpc.Client 默认不支持并发请求?
Go 标准库的 rpc.Client 在调用 Call 或 Go 时,底层复用同一个 TCP 连接,但响应没有绑定请求 ID —— 它靠「顺序读取」匹配:第 N 个响应一定对应第 N 个发出的请求。一旦并发调用,响应就可能错位,轻则返回错误结果,重则 panic(比如解包失败)。
这不是 bug,是设计使然:标准 rpc 包假设单线程串行使用。想并发,必须自己加请求 ID 和响应路由逻辑。
怎么给每个请求打上唯一 seq 并关联响应?
核心是在发送前注入自增序号,在响应体里回传该序号,再由客户端按序号分发给对应等待的 goroutine。标准 rpc 协议不带这个字段,所以得自己封装一层。
- 定义一个带
Seq字段的包装结构体,如type RequestWrapper struct { Seq uint64; Payload interface{} } - 服务端收到后,原样透传
Payload到业务方法,但响应时也包一层ResponseWrapper{Seq: req.Seq, Result: ...} - 客户端发请求前,用原子计数器生成
Seq,并把chan或func回调存进 map:pending[seq] = make(chan *ResponseWrapper, 1) - 启动一个单独 goroutine 专责读响应,从连接里反序列化出
ResponseWrapper,然后往pending[wr.Seq]写入
注意:seq 必须全局唯一(跨 goroutine),推荐用 atomic.AddUint64(&nextSeq, 1),别用锁或 rand。
立即学习“go语言免费学习笔记(深入)”;
net/rpc 的 Client.Go 能直接用吗?
不能直接用于多路复用场景。虽然 Client.Go 返回 *rpc.Call,看起来可并发,但它内部仍依赖顺序读响应 —— 所有 Go 调用共享同一个 client.in reader,没有隔离机制。
- 如果你强行并发调用
Client.Go,且服务端响应耗时不均,大概率出现invalid character或unexpected EOF错误(因为 JSON/GOB 解码器读到了不属于它的字节) - 即使服务端用
jsonrpc2或自定义协议,只要没在协议层嵌入seq,标准rpc.Client就无法安全复用连接 - 替代方案:要么改用
gRPC(天然支持 stream + request ID),要么自己实现带 seq 的 client 封装(见上一节)
性能和连接复用要注意什么?
加了 seq 路由后,并发能力有了,但容易忽略两个隐性开销:
- map 查找和 channel 发送本身有成本,高 QPS 下建议用
sync.Pool复用RequestWrapper和ResponseWrapper实例,避免 GC 压力 - 如果每个请求都新建连接,再多路也没意义 —— 务必复用
net.Conn,并在连接断开时清空pendingmap(否则 goroutine 泄漏) - 超时控制要下放到单个请求粒度:不能只设
conn.SetDeadline,得给每个pending[seq]配time.AfterFunc清理,否则超时请求会永远卡住
真正难的不是加 seq,而是让整个生命周期(发、等、收、超时、重连、清理)全部对齐,稍有遗漏就会静默丢响应或泄漏 goroutine。









