当前位置:首页 > 文章列表 > Golang > Go问答 > Connect Go 的进程内传输怎么测试 RPC 而不监听端口

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

来源:17golang原创 2026-11-11 03:38:49 0浏览 收藏

我第一次把 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删除
Go net/mail.Header.AddressList 缺少 From 字段时怎样区分空值与错误Go net/mail.Header.AddressList 缺少 From 字段时怎样区分空值与错误
上一篇
Go net/mail.Header.AddressList 缺少 From 字段时怎样区分空值与错误
Go cookiejar.Options.PublicSuffixList 怎么兼容内网测试域名
下一篇
Go cookiejar.Options.PublicSuffixList 怎么兼容内网测试域名
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    972次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    980次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    1095次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    1068次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    906次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码