我们已经把 go-zero 的核心肌理几乎全部拆解了一遍:从 goctl 的代码生成,到 REST 和 zRPC 的请求处理链路,再到服务发现、弹性保护、可观测性与数据访问层。这些能力构成了一个微服务框架的基础设施——你可以在上面构建出一个完整的微服务系统。
但是,框架的真正活力在于扩展性。当核心能力稳定之后,自然会涌现出在核心框架之上构建更高层抽象的需求。go-zero 的 gateway 包和 mcp 包就是这种思路的两个典型产物:它们不修改 rest 或 zrpc 的一行代码,却分别构造出了协议网关和 AI 工具协议服务两种全新的服务形态。
这两个模块之所以值得放在同一篇文章中讨论,是因为它们共享了完全相同的设计策略:
- 以
rest.Server为 HTTP 底座:Gateway 和 MCP 都不自己处理 HTTP 细节,而是利用已经在生产环境中验证过的 REST 引擎来管理启动、路由、中间件和优雅退出。 - 函数选项模式(Functional Options):通过
WithHeaderProcessor、WithMiddleware、WithRequestMetadataExtractor等 Option 函数注入定制行为,保持核心构造函数的简洁。 - 职责单一的配置结构体:嵌入
rest.RestConf,再叠加各自领域的特定字段,既不重复造轮子,又能表达领域语义。
我们先从 Gateway 讲起——它是两个模块中更"重"的一个,涉及协议转换(HTTP ↔ gRPC、HTTP ↔ HTTP)的完整实现;然后再看 MCP 如何以更轻盈的方式,将一个 gRPC 服务的某个能力注册为 AI 可调用的工具。
Gateway:为什么需要协议转换网关
HTTP 客户端能直接调用 gRPC 服务吗
在一个典型的微服务架构中,后端服务通常使用 gRPC 相互通信,但前端(浏览器、移动端)和外部的第三方系统只能发 HTTP 请求。这就产生了一个问题:外部 HTTP 请求如何调用内部 gRPC 服务?
gRPC 使用 Protobuf 序列化和 HTTP/2 传输——浏览器原生不支持。传统做法是写一个专门的"翻译层":对每个 gRPC 方法写一个 REST handler,在其中手动序列化参数、调用 gRPC、再把结果反序列化。随着服务数量的增长,这个翻译层会变成维护负担最重的代码——因为它不包含业务逻辑,却要针对每个接口重复编写。
go-zero 的 Gateway 就是为了解决这个问题。它的核心能力是:自动将 HTTP 请求转换为 gRPC 调用,或将 HTTP 请求转发到下游 HTTP 服务,而无需手写任何"翻译"代码。
先看它的顶层结构:
1 | // gateway/server.go |
Server 通过嵌入 *rest.Server 获得了完整的 HTTP 服务能力——端口监听、路由匹配、中间件链、优雅退出——所有这些都是免费继承的。Gateway 自身只需要关注"上游服务是什么"以及"如何把 HTTP 请求转换并转发"。
它的配置也清晰地反映了这种设计:
1 | // gateway/config.go |
你会注意到 Grpc 和 Http 通过 optional=!grpc 标记为互斥——一个上游要么是 gRPC 服务,要么是 HTTP 服务,不能同时是两者。这个约束在 build() 方法中也得到了相应的处理。
服务生命周期:Start 与 Stop 的顺序考量
在深入路由构建逻辑之前,先看一下 Gateway 的生命周期管理,这体现了它在资源管理上的细致考量:
1 | func (s *Server) Start() { |
Start 中,build() 读取上游配置并构建路由表,然后将路由注册到底层 rest.Server,最后启动 HTTP 监听。这个顺序意味着路由构建是一次性的——Gateway 在启动后不再动态变更路由表,所有上游服务在启动时就必须确定。
Stop 中的顺序则更有意味:先关 HTTP,再关 gRPC。源码注释解释了原因:如果反过来先关 gRPC,那么 HTTP 服务器还可能继续接受请求并尝试转发这些请求到已经不存在的 gRPC 连接上。先停入口、再清理上游连接,能保证不会有"请求还在处理中但连接已经断开"的竞态窗口。所有 gRPC 连接通过 threading.NewRoutineGroup 并发关闭,避免逐个关闭的串行延迟。
路由构建:MapReduce 如何编排两个上游体系
Gateway 最核心的代码在 build() 方法中。这里使用了一个 mr.MapReduceVoid 来并行构建路由——这是一个值得关注的工程选择:
1 | func (s *Server) build() error { |
三个回调函数分别对应 MapReduce 的 Generate、Map 和 Reduce 三个角色:
- Generate:将上游列表逐个发送到
sourcechannel。 - Map:对每个上游,根据其类型(gRPC 或 HTTP),调用对应的路由构建方法,并将生成的路由写入
writer。 - Reduce:从
pipechannel 接收所有构建好的路由,统一注册到rest.Server。
这里使用 mr.MapReduceVoid 的动机很明确:当有多个上游服务时,每个上游的路由构建(特别是基于 protoset 文件或 gRPC reflection 的解析)可能是耗时的。比如 gRPC 上游的分支需要建立连接、获取服务描述符、遍历所有方法并解析 HTTP 注解——多个上游并行处理能显著缩短启动时间。
gRPC 协议转换:从 HTTP 请求到 Protobuf 调用的全过程
两步信息获取:先拿到描述符,再从描述符中提取路由
对于 gRPC 上游的转换,Gateway 需要知道三件事:
- 上游服务提供了哪些 RPC 方法
- 每个方法的请求/响应消息类型是什么(用于序列化/反序列化)
- 如果有 HTTP 注解(
google.api.http),对应的 HTTP 方法和路径是什么
这些信息通过 grpcurl 的描述符源(Descriptor Source)获得。Gateway 支持两种获取方式:
1 | func createDescriptorSource(cli zrpc.Client, up Upstream) (grpcurl.DescriptorSource, error) { |
ProtoSet 模式(up.ProtoSets 不为空):使用 protoc --descriptor_set_out 编译好的 proto 描述符文件,包含完整的服务定义和消息类型。这种方式不需要服务端开启 gRPC reflection,启动时直接从文件加载。
Reflection 模式(up.ProtoSets 为空):通过 gRPC Server Reflection 协议动态获取服务定义。grpcreflect.NewClientAuto 会自动创建反射客户端,从 gRPC 服务端实时拉取服务描述符。
拿到描述符源之后,下一步是从中提取可路由的方法:
1 | // gateway/internal/descriptorsource.go |
这段代码的核心逻辑是:遍历每个 gRPC 服务的每个方法,尝试从 Proto 的方法选项中提取 google.api.http 注解。这个注解是 gRPC-Gateway 生态的标准,定义了 RPC 方法对应的 HTTP 路由。
如果你的 proto 文件中写入了这样的注解:
1 | import "google/api/annotations.proto"; |
那么 Gateway 解析后会自动生成路由 POST /v1/greeter/:name → Greet/SayHello——注意 {name} 被 adjustHttpPath 替换为 :name,这是因为 go-zero 的 REST 路由使用 :param 作为路径参数格式,而不是 gRPC HTTP 注解的 {param} 格式。
除此之外,Upstream.Mappings 还允许你手动指定路由映射——这对于没有在 proto 中写入 HTTP 注解的场景(或需要覆盖默认映射时)非常有用。Mappings 的每个条目会作为独立的 Route 注册到 REST 引擎:
1 | for _, m := range up.Mappings { |
一次请求的完整旅途:解析、调用与响应
当 Gateway 接收到一个匹配到 gRPC 上游路由的 HTTP 请求时,buildGrpcHandler 返回的 handler 负责完成整个协议转换过程。我们可以把这个过程分为三个阶段:
第一阶段——请求解析。internal.NewRequestParser 负责将 HTTP 请求体转换为 gRPC 消息结构。这里需要综合多个数据源:
1 | // gateway/internal/requestparser.go |
这里有三种组合:纯参数(无 body)、纯 body(无参数)、混合数据(body + 参数/路径变量)。在混合模式下,先反序列化 body 为 map,再用参数/路径变量覆盖同名字段——路径变量拥有最高优先级。这个处理方式意味着:你的 HTTP 请求可以通过 query 参数或路径变量补充/覆盖请求体中的字段。
最终,这些数据通过 jsonpb.Unmarshaler(带 AllowUnknownFields: true)反序列化——允许未知字段的好处是,当 proto 定义升级后,Gateway 不会因为客户端多传了字段就拒绝请求。
第二阶段——发起 RPC 调用。解析完成后,核心调用只有一行:
1 | grpcurl.InvokeRPC(r.Context(), source, cli.Conn(), rpcPath, |
grpcurl.InvokeRPC 是 github.com/fullstorydev/grpcurl 库提供的关键功能:它能根据描述符源动态调用 gRPC 方法——不需要生成任何客户端 stub 代码。参数依次是:请求上下文、服务描述符、gRPC 连接、RPC 全路径(格式 package.Service/Method)、gRPC metadata、响应处理器和请求数据提供者。
第三阶段——响应处理。internal.NewEventHandler 实现了 grpcurl.InvocationEventHandler 接口,负责将 gRPC 的响应体(Protobuf 消息)序列化为 JSON 并写入 HTTP 响应。同时,它会将 gRPC metadata header 和 trailer 分别映射为 HTTP 响应头:
1 | // gateway/internal/eventhandler.go |
Header 的处理:Metadata 传递与 Trace 传播
将 HTTP header 转换为 gRPC metadata 是 Gateway 的一个重要设计点。prepareMetadata 方法将这一过程分为两步:
1 | func (s *Server) prepareMetadata(header http.Header) []string { |
ProcessHeaders 的职责分成两条线索:
线索一——Grpc-Metadata- 前缀:HTTP 请求头中以 Grpc-Metadata- 开头的字段,会被去掉前缀并转换为小写后作为 gRPC metadata 传递。这种机制让 HTTP 客户端可以通过请求头直接影响 gRPC 调用的 metadata 参数。
线索二——OpenTelemetry Trace 传播:trace 相关的请求头(traceparent、tracestate、baggage)会被直接转发为 gRPC metadata,确保分布式追踪在穿越 Gateway 后仍然连续。这是关键的设计细节——如果没有这层转发,Gateway 就成了 trace 链路上的"断点",发起方的 trace ID 无法传递到下游 gRPC 服务。
1 | var traceHeaders = map[string]bool{ |
HTTP 代理:简洁的请求转发
问题:gRPC Gateway 解决了纯 HTTP 转发的需求吗
Gateway 的 HTTP 上游模式处理的场景与 gRPC 模式完全不同:上游服务本身就是一个 HTTP 服务,Gateway 的角色退化为一个反向代理(reverse proxy)。如果后端服务已经暴露了 HTTP 接口——比如一个第三方的 REST API 或一个内部的老旧服务——你不需要做协议转换,只需要做路径映射和请求转发。
相比 gRPC 模式经由 grpcurl 的复杂路径,HTTP 模式的 handler 构建要简洁得多:
1 | func (s *Server) buildHttpHandler(target *HttpClientConf) http.HandlerFunc { |
buildRequestWithNewTarget 是其中的关键。它从原始 HTTP 请求构建一个新的目标指向上游的请求:
1 | func buildRequestWithNewTarget(r *http.Request, target *HttpClientConf) (*http.Request, error) { |
操作可以归结为两步:替换目标地址,以及可选地添加路径前缀。Target 字段支持 host:port 格式的地址,Prefix 用于为上游服务统一添加路径前缀。
一个关键细节是 WithContext:新请求继承原始请求的 context,因此原始请求的 deadline、取消信号和 trace 上下文都会贯穿整个转发过程。这就是为什么 TargetConf 的 Timeout 会在原始请求 deadline 的基础上再创建 context.WithTimeout 的子 context——它并不会违反原始请求的取消约定。
中间件支持
Gateway 通过 WithMiddleware Option 提供了中间件注入能力:
1 | func (s *Server) buildChainHandler(handler http.HandlerFunc) http.HandlerFunc { |
倒序遍历中间件列表是为了保证中间件的洋葱模型执行顺序(先注册的先执行,后注册的后执行)——这和我们熟悉的 rest/chain.Chain 的实现方式一致。每个中间件包裹内部 handler,最终形成一层层嵌套。
MCP:当框架需要"对话" AI
问题:go-zero 的 REST/zRPC 底座如何承载 AI 工具协议
如果说 Gateway 是 go-zero 在传统微服务领域的一次"横向扩展"——让 HTTP 请求可以访问 gRPC 服务——那么 MCP 包则是一次更有想象力的"纵向突破":它让 go-zero 的服务可以暴露为 AI 模型可调用的工具。
Model Context Protocol(MCP)是一个由 Anthropic 提出的开放协议,用于标准化 AI 模型与外部工具/数据源之间的交互。MCP 定义了三种传输方式:stdio(进程通信)、SSE(Server-Sent Events,基于 2024-11-05 规范)和 Streamable HTTP(基于 2025-03-26 规范)。go-zero 的 MCP 实现选择了用 HTTP 服务承载后两种传输方式——这正是它能复用 rest.Server 的原因。
1 | // mcp/server.go |
你可以把它看作一个"双层结构":外层是 go-zero 的 rest.Server,负责 HTTP 生命周期、路由、超时和 CORS;内层是官方的 sdkmcp.Server,负责 MCP 协议本身——Tool/Prompt/Resource 的注册、JSON-RPC 消息的路由和 Schema 的自动生成。
传输层选择:SSE 还是 Streamable HTTP
MCP 服务通过 McpConf.Mcp.UseStreamable 字段选择传输方式。这一选择决定了路由注册的方式和底层使用的 MCP SDK handler 类型:
1 | func (s *mcpServerImpl) setupSSETransport() { |
两种传输方式共用同一个路由注册函数:
1 | func (s *mcpServerImpl) registerRoutes(handler http.Handler, endpoint string) { |
无论选择哪种传输方式,同一个路径上都会同时注册 GET 和 POST 两个路由。这是 MCP 协议的要求:GET 用于建立 SSE 长连接(客户端通过 GET 订阅服务器推送的事件流),POST 用于发送 JSON-RPC 请求(客户端通过 POST 发送 initialize、tools/list、tools/call 等 MCP 协议消息)。
两种方式的区别在于底层的消息传递模型:
- SSE 模式(
UseStreamable: false):GET 端点建立持久连接(默认 24 小时超时),服务器通过这个连接向客户端推送事件。POST 端点用于客户端发送请求。这是 2024-11-05 规范的实现。 - Streamable HTTP 模式(
UseStreamable: true):将 SSE 和消息请求融合到一个更统一的双向模型中。这是 2025-03-26 规范的新传输方式,在某些场景下支持更高效的服务端推送。
请求元数据的传递桥梁
在实践中,MCP 工具往往需要感知请求上下文——租户 ID、用户标识、环境标记。这些信息不在 MCP 协议标准中,必须由 go-zero 的 HTTP 层提取后传递给 MCP handler。
go-zero 通过 RequestMetadataExtractor 解决了这个问题:
1 | // mcp/options.go |
wrapRequestMetadata 方法在每个 HTTP 请求处理前提取元数据并注入 context.Context:
1 | func (s *mcpServerImpl) wrapRequestMetadata(next http.Handler) http.Handler { |
在 MCP 工具 handler 中,可以通过以下几个辅助函数读取这些元数据:
HeaderFromContext(ctx, "X-Tenant-Id")— 获取 HTTP 请求头QueryFromContext(ctx, "trace")— 获取 URL 查询参数PathFromContext(ctx, "scope")— 获取路径变量(如/sse/:scope中的:scope)RequestMetadataFromContext(ctx)— 一次性获取所有元数据
这个设计与 go-zero REST handler 中的 httpx.Parse 参数解析是同样的理念:HTTP 层的信息被整理、规范化后,通过 context 传递给业务逻辑,业务 handler 不需要直接依赖 *http.Request,也不需要了解 HTTP 的传输细节。
类型安全的工具注册
注册 MCP Tool 的方式体现了类型安全与便利性的平衡。通过 Go 泛型,工具 handler 可以在编译时确定参数类型,而 JSON Schema 由 SDK 根据 struct tag 自动生成:
1 | // mcp/types.go |
类型参数 In 是工具的输入参数类型,Out 是结构化输出的类型。SDK 会根据 In 类型的 struct tag 自动生成 JSON Schema——例如 json:"name" 和 jsonschema:"description=Name of the person to greet"——客户端可以据此理解工具的输入契约。Out 类型用于生成结构化输出 Schema,让 AI 模型知道工具返回的数据结构,从而做出更精确的后续行为决策。
AddTool 的类型断言 server.(*mcpServerImpl) 是一个防护中间层:如果传入的不是 mcpServerImpl(比如你自己实现了一个 McpServer 接口),它会记录错误而不是 panic。这种做法让 mcp.AddTool 对于框架的扩展性保持友好。
两个模块的设计共性
共用的"底座 + 插件"架构
Gateway 和 MCP 虽然解决的业务问题完全不同——一个是协议转换,一个是 AI 协议服务——但它们在架构上使用了同一套模式:
1 | ┌─────────────────────────────┐ |
这个架构的好处是关注点分离。HTTP 基础设施(监听端口、TLS、超时、优雅关闭、CORS)是在生产环境中经过充分验证的 rest.Server 负责的——不需要在 Gateway 或 MCP 中重复实现。而每个模块只需要关注自己的领域逻辑:Gateway 关注协议转换,MCP 关注协议适配。
Options 模式:相同的扩展哲学
两个模块都使用 functional options 模式来提供扩展点:
1 | // Gateway |
这个模式的核心优势是:构造函数保持简洁(只接受必需的配置参数),所有可选的定制行为通过 Option 函数注入。当将来需要增加新的扩展点时,只需要添加新的 Option 函数,不影响已有的调用代码。
配置设计:嵌入与叠加
两者的配置都采用"嵌入基础配置 + 叠加领域配置"的模式:
1 | type GatewayConf struct { |
这种配置设计让 go-zero 的用户在使用这两个模块时有着一致的体验:先用标准的 REST 配置定义服务基本属性,再添加各自领域的配置。对于那些已经熟悉了 REST 服务配置的人来说,转用 Gateway 或 MCP 几乎零学习成本。
示例串联:让一条请求从 Gateway 到 MCP 工具调用
现在让我们用一个完整的示例来串联 Gateway 和 MCP 的能力。假设你有一个 gRPC 订单服务 OrderService,我们先用 Gateway 把它暴露为 HTTP 接口,再注册一个 MCP Tool 让 AI 能查询订单状态。
Gateway 配置与启动
首先编写 Gateway 配置,将一个 gRPC 服务的 GetOrder 方法暴露为 HTTP 路由:
1 | Name: order-gateway |
Gateway 会从 order.pb 中加载服务描述符,解析出 GetOrder 方法的请求/响应消息类型,然后将 GET /api/orders/:orderId 绑定到该 RPC 方法。当客户端发来 HTTP 请求时,请求解析器将路径变量 :orderId 注入到解析后的数据中,经由 grpcurl.InvokeRPC 调用 gRPC 方法,再将 Protobuf 响应序列化为 JSON 返回。
MCP 服务配置与 Tool 注册
接下来,我们把同一个 gRPC 服务的订单查询能力注册为 MCP Tool:
1 | type QueryOrderArgs struct { |
这里的 handler 通过 mcp.HeaderFromContext 从 HTTP 请求头中获取租户 ID——这个信息在 MCP 标准协议之外,通过 go-zero 的 metadata bridge 传递给 handler。注意 AddTool 的 Out 类型参数是 protobuf 的 GetOrderResponse:SDK 会自动为它生成 JSON Schema,AI 模型就能理解这个工具返回的结构化数据,进而做出更精准的决策。
运行时刻的数据流
完整的数据流如下:
1 | AI Client (MCP SDK) |
从 Gateway 的协议转换到 MCP 的 AI 工具暴露,底层的 rest.Server、zrpc.Client、配置加载和优雅退出机制都是相同的。上层形态不同,下层基础一致——这就是在成熟框架之上构建扩展模块的威力。
小结
Gateway 和 MCP 是 go-zero 核心框架能力之上的两个扩展形态。它们验证了一个基本判断:当 rest 和 zrpc 的抽象足够稳定、配置体系和生命周期管理足够成熟之后,在其上构建新的服务形态是水到渠成的事。
Gateway 处理的是**“协议边界”**的问题——将 gRPC 服务暴露为 HTTP 接口,为此它实现了一套完整的描述符解析、请求转换和响应映射机制。这套机制的核心依赖是 grpcurl 的 InvokeRPC——它使得 Gateway 可以在不生成任何 gRPC 客户端 stub 代码的情况下,动态调用任意的 gRPC 服务。
MCP 处理的是**“工具边界”**的问题——将 go-zero 的服务能力暴露为 AI 可调用的工具。它的核心依赖是官方 MCP SDK,但通过 rest.Server 的集成和 RequestMetadata bridge,它让 MCP 工具能自然地融入 go-zero 的 HTTP 基础设施中。
从这两个模块中,我们可以提炼出 go-zero 扩展体系的核心方法:
- 嵌入复用:通过嵌入
rest.RestConf和*rest.Server继承 HTTP 能力 - Option 注入:通过 functional options 提供可组合的扩展点
- 配置分层:基础配置 + 领域配置的叠加模式
- Adapter 模式:Gateway 和 MCP 本质上是不同协议的适配器,它们将外部协议映射到 go-zero 内部的
rest.Route/zrpc.Client体系
下一篇(最后一篇)我将全盘复盘:从测试策略到性能分析工具,从框架扩展点到设计哲学,为整个十五篇系列做一个系统性的收束。