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

用 net/rpc 搭建本地 RPC 服务做单元测试
Go 标准库的 net/rpc 支持内存通道(rpc.NewServer() + server.ServeCodec())直接对接 gob.NewEncoder/Decoder,无需启动 TCP 端口就能完成完整 RPC 流程。这是本地测试最轻量、最可控的方式。
- 避免端口占用和网络抖动干扰,测试稳定
- 服务端和客户端都在同一进程,可直接断点调试双方逻辑
- 不依赖外部依赖(如 etcd、consul),适合 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 读写错位,都会让测试看似“跑通”实则没触发任何业务逻辑。










