登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

Connect Go 的进程内传输怎么测试 RPC 而不监听端口

来源:17golang原创

时间:2026-11-11 03:38:49 291浏览 收藏

我第一次把 Connect Go 的 RPC 单测从“直接调用 handler 方法”改成客户端调用时,最明显的变化不是代码变长,而是测试开始覆盖真正的 Connect 调用边界:请求会经过 connect.Server,客户端和服务端拦截器会执行,handler 读到的上下文也带有 Connect 的调用信息。这个过程不需要监听端口。

官方地址:https://connectrpc.com/docs/go/in-process-transport/

做法是使用 connectrpc.com/connect/v2/connectinprocess。先注册生成的服务处理器,再用 connectinprocess.New(server) 创建 transport,最后把 transport 交给生成的客户端。它适合验证 RPC 业务路径,但不代替真实 HTTP、协议和监听测试。

生成的 Connect 客户端经过客户端拦截器和 connectinprocess Transport 到达 connect Server 与服务处理器的关系图
图1:Connect Go 进程内测试的静态拓扑示意,传输不监听端口但仍经过客户端、服务端和处理器边界。

为什么不建议直接调用服务方法

直接调用 Greet(ctx, req) 看起来最省事,但它绕过了 Connect 服务端负责的那一层。官方测试文档明确建议通过客户端和 transport 调用服务,因为直接调用时上下文没有 CallInfo,拦截器也不会运行。

这也是进程内 transport 的价值:它把“业务逻辑单测”和“完整网络测试”之间的空档补上。你仍然可以在一个进程里完成请求和响应,但测试入口已经更接近生产调用方。

构造一个不监听端口的客户端

假设 Protobuf 已经生成了 greetv1connect 客户端与注册函数,最小构造方式如下。这里的 GreetServer 只代表你的业务实现,生成代码不需要手写。

package greet_test

import (
    "context"
    "testing"

    "connectrpc.com/connect"
    "connectrpc.com/connect/v2/connectinprocess"
    greetv1 "example.com/greet/gen/greet/v1"
    greetv1connect "example.com/greet/gen/greet/v1/greetv1connect"
)

func newInProcessClient() greetv1connect.GreetServiceClient {
    // 只创建 Connect 服务容器,不创建 mux,也不绑定监听地址。
    server := connect.NewServer()
    greetv1connect.RegisterGreetServiceHandler(server, &GreetServer{})

    // transport 把 RPC 直接分派到同进程的 server。
    transport := connectinprocess.New(server)
    return greetv1connect.NewGreetServiceClient(
        connect.NewClient(transport),
    )
}

func TestGreetInProcess(t *testing.T) {
    client := newInProcessClient()

    // 使用测试上下文发起真实的 Connect 客户端调用。
    got, err := client.Greet(context.Background(), &greetv1.GreetRequest{Name: "Jane"})
    if err != nil {
        t.Fatalf("Greet: %v", err)
    }
    if want := "Hello, Jane!"; got.GetGreeting() != want {
        t.Fatalf("Greeting = %q, want %q", got.GetGreeting(), want)
    }
}

关键点只有三个:connect.NewServer() 持有服务端状态,注册函数把 handler 接到这个 server 上,connectinprocess.New 再把它包装成客户端可以使用的 transport。整个测试没有 Listen、没有端口,也没有 HTTP 请求。

用客户端入口覆盖请求、拦截器和错误

我在实际测试里最容易漏掉的是拦截器。直接调 handler 时,日志、鉴权、指标或 tracing 拦截器都不会出现;改为 in-process client 后,客户端和服务端两侧的拦截器都可以进入测试范围。官方文档还说明,拦截器的顺序遵循洋葱模型:最先提供的拦截器是最外层。

func newClientWithInterceptors() greetv1connect.GreetServiceClient {
    // 客户端拦截器可记录调用方看到的开始、结束和错误。
    clientLogger := connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc {
        return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
            // 真实项目中可在这里写入测试可观察的计数或 trace 标记。
            res, err := next(ctx, req)
            return res, err
        }
    })

    server := connect.NewServer()
    // 注册服务时传入服务端拦截器,验证 handler 前后的服务端边界。
    greetv1connect.RegisterGreetServiceHandler(
        server,
        &GreetServer{},
        connect.WithInterceptors(clientLogger),
    )

    // 进程内 transport 保留客户端到服务端的 Connect 调用链。
    return greetv1connect.NewGreetServiceClient(
        connect.NewClient(
            connectinprocess.New(server),
            connect.WithInterceptors(clientLogger),
        ),
    )
}

上面把同一个示例拦截器放在两端只是为了展示位置;真实项目通常会分别准备客户端和服务端拦截器。若你的生成代码版本把 handler 选项放在其他构造函数中,保持原则不变:让拦截器挂在生成的客户端或 handler 上,再经由 connectinprocess.New 发起调用。

消息会复制,不能用共享指针假设来写断言

进程内调用容易让人误以为“客户端和 handler 拿到的是同一个 Protobuf 指针”。Connect 官方为了保持网络传输的隔离语义,默认会用 connectinprocess.ProtoCopy 深拷贝请求和响应。handler 修改自己的请求副本,不应该反向修改客户端手里的请求。

因此,测试断言应该关注字段值、响应和错误,而不是比较请求指针地址。若业务确实要替换复制策略,可以在创建 transport 时使用 connectinprocess.WithCopyFunc;这属于明确的性能或对象策略选择,不应在普通单测里随意打开。

Connect Go 进程内传输通过 ProtoCopy 隔离客户端请求与服务端请求并保留远程错误和无缓冲流关系图
图2:进程内传输保留网络调用所需的消息隔离、远程错误和流式背压语义的静态关系图。

错误和元数据要按 RPC 语义检查

进程内 transport 不是普通函数调用:handler 返回的错误会以远程错误语义到达客户端。测试时可以用 connect.CodeOf 断言错误码,不要只比较错误字符串。上下文取消和 deadline 到期也会保留相应的 Connect code。

func TestGreetError(t *testing.T) {
    client := newInProcessClient()

    // 空名称由 handler 返回业务错误,测试只关心客户端看到的错误码。
    _, err := client.Greet(context.Background(), &greetv1.GreetRequest{})
    if err == nil {
        t.Fatal("Greet error = nil, want error")
    }
    if got, want := connect.CodeOf(err), connect.CodeInvalidArgument; got != want {
        t.Fatalf("error code = %s, want %s", got, want)
    }
}

同理,若测试了 response header、trailer 或服务端设置的 metadata,要从生成客户端暴露的响应对象或调用上下文中读取。不要把没有真实 HTTP 对端信息误判成 transport 失效:进程内调用没有 HTTP 请求,connecthttp.ServerInfoForContext 会报告没有 HTTP server info,某些协议和 peer 字段也会为空。

流式 RPC 需要留意无缓冲背压

Connect 的进程内 transport 对流式 RPC 会使用独立 goroutine 和无缓冲 channel。发送方要等接收方取走消息才会继续,这与网络 transport 的背压方向一致,但不是“把所有消息放进内存队列”。如果测试只发送不接收,出现阻塞是预期的调用关系,应该让测试同时推进发送和接收。

普通 unary RPC 则会同步分派,不需要为了一个简单的请求额外启动 goroutine。我的经验是先用 unary 测试验证 handler、拦截器和错误,再为流式方法单独设计接收循环和取消条件,这样失败时更容易看出是业务错误还是测试没有消费消息。

哪些问题仍然要用真实 HTTP 测试

进程内 transport 适合做快速、稳定的 RPC 语义测试,但它不会替你覆盖线路本身。下面这些问题仍应安排到真实服务器测试中:

  • HTTP 方法、URL 路径、协议选择、压缩和真实序列化是否符合预期。
  • 监听地址、TLS、HTTP/2、代理、超时和连接复用是否正常。
  • 网关、负载均衡或中间件如何改写 header、trailer 和错误响应。

换句话说,进程内测试解决的是“Connect 调用链和业务 handler 是否正确”,真实网络测试解决的是“请求离开进程后还能否按协议到达”。两者不是互相替代,而是把测试成本放在不同边界上。

最后的选择建议

测试目标入口是否需要端口重点断言
服务业务和 Connect 调用链生成客户端 + connectinprocess不需要响应、错误码、拦截器、上下文
消息复制和流式背压生成客户端 + connectinprocess不需要请求隔离、接收顺序、取消
HTTP/协议/监听行为真实 Connect HTTP server需要路径、协议、header、连接和部署

如果当前测试只是为了确认“服务收到请求后返回什么”,直接用进程内 transport 往往是更合适的起点;如果断言涉及 HTTP 线路,就不要把 in-process 的空 peer 信息当成真实网络结果。

相关问题

Connect Go 进程内传输会不会完全跳过拦截器?

不会。官方文档说明客户端和服务端拦截器都会运行,前提是你把它们挂在对应的客户端或 handler 上。

handler 修改请求后客户端会看到变化吗?

默认不会。进程内 transport 会对请求和响应做深拷贝,以保持与网络传输相近的内存隔离。

为什么 in-process 测试拿不到真实 peer 地址?

因为 RPC 没有离开进程,也没有真实 HTTP 对端。需要检查协议、连接或 peer 地址时,应补充真实服务器测试。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>