如何在Golang中模拟RPC调用_本地测试实现技巧

9次阅读

用 net/rpc 搭建本地 RPC 服务做单元测试的最轻量方式是使用 bytes.Buffer 模拟传输层,配合 gob 编解码,无需网络端口;需确保结构体字段导出、使用独立读写 buffer、正确启停协程,并注意 codec 一致性与 goroutine 生命周期管理。

如何在Golang中模拟RPC调用_本地测试实现技巧

net/rpc 搭建本地 RPC 服务做单元测试

Go 标准库net/rpc 支持内存通道(rpc.NewServer() + server.ServeCodec())直接对接 gob.NewEncoder/Decoder,无需启动 TCP 端口就能完成完整 RPC 流程。这是本地测试最轻量、最可控的方式。

  • 避免端口占用和网络抖动干扰,测试稳定
  • 服务端和客户端都在同一进程,可直接断点调试双方逻辑
  • 不依赖外部依赖(如 etcdconsul),适合 CI 环境
  • 注意:必须确保注册的服务结构体字段全部是导出字段(首字母大写),否则 gob 编码失败

bytes.Buffer 替代网络连接实现零开销编解码

RPC 的核心是请求序列化 → 传输 → 反序列化 → 执行 → 返回序列化 → 传输 → 反序列化。用 bytes.Buffer 模拟“传输层”,能跳过 socket 创建、超时、重连等无关逻辑,只验证业务协议是否正确。

buf := new(bytes.Buffer) server := rpc.NewServer() server.RegisterName("Arith", &Arith{}) codec := gob.NewGobServerCodec(buf, buf) // 注意:两个 buf 分别用于读/写 go server.ServeCodec(codec) 

// 客户端复用同一个 buf 做通信 client := rpc.NewClientWithCodec(gob.NewGobClientCodec(buf, buf)) defer client.Close()

args := &ArithArgs{A: 10, B: 3} var reply ArithReply err := client.Call("Arith.Multiply", args, &reply)

  • 必须用两个独立的 bytes.Buffer 实例(或一个但显式 Reset()),否则读写位置冲突导致 panic
  • gob 是 Go 默认 codec,若服务端用 jsonrpc,客户端也得用 jsonrpc.NewClientCodec
  • 调用前未调用 server.ServeCodec() 启动协程,会导致 Call 阻塞或立即返回 io.EOF

测试异步通知类方法(如订阅、回调)要手动控制执行顺序

标准 net/rpc 不支持服务端主动推消息。若业务中有类似 Subscribe(req *Req, stream Stream) 这种伪流式接口,本地测试时需把 stream 替换为自定义结构体,内部用 chan 模拟推送行为。

type MockStream struct {     ch chan interface{} } 

func (m *MockStream) Send(v interface{}) error { m.ch <- v return nil }

// 在测试中: stream := &MockStream{ch: make(chan interface{}, 10)} go func() { // 模拟服务端往 stream 写数据 stream.Send(&Event{ID: "e1"}) stream.Send(&Event{ID: "e2"}) }()

// 客户端从 stream 接收 for i := 0; i < 2; i++ { select { case ev := <-stream.ch: // 处理事件 } }

  • channel 容量不能为 0,否则 Send 会阻塞,测试卡死
  • 务必在 goroutine 中调用 Send,否则同步调用会等客户端接收,形成死锁
  • 真实 RPC 框架(如 gRPC)有原生流支持,但标准库没有,强行模拟仅适用于协议兼容性验证

替换真实 RPC 客户端为 mock 结构体更简单

如果只是想绕过网络调用验证上层逻辑(比如某个 handler 里调用了 userSvc.GetUser()),直接定义一个符合接口的 mock 结构体比启动本地 RPC 服务更轻量。

立即学习go语言免费学习笔记(深入)”;

type MockUserSvc struct {     GetUserFunc func(ctx context.Context, id int64) (*User, error) } 

func (m MockUserSvc) GetUser(ctx context.Context, id int64) (User, error) { return m.GetUserFunc(ctx, id) }

// 测试中: svc := &MockUserSvc{ GetUserFunc: func(ctx context.Context, id int64) (*User, error) { return &User{Name: "test"}, nil }, } handler := NewHandler(svc) // 调用 handler 方法,内部会走 mock 的 GetUser

  • 适合逻辑分层清晰、RPC 客户端已抽象为接口的项目
  • 比启动 RPC 服务快一个数量级,尤其适合参数组合多、case 密集的场景
  • 无法覆盖 codec 序列化错误、网络超时、服务端 panic 等真实链路问题,需另配集成测试

本地测试 RPC 最容易忽略的是 codec 一致性与 goroutine 生命周期管理——服务端没启协程、客户端没关连接、buffer 读写错位,都会让测试看似“跑通”实则没触发任何业务逻辑。

text=ZqhQzanResources