0%

go-zero 源码分析 02:整体架构与源码地图

这篇文章我们会从仓库顶层结构出发,逐步拆解到四层架构,然后用 REST 和 RPC 两条主调用链示范 请求到底穿过了哪些层,最后归纳出贯穿整个项目的几种设计模式。读完这篇文章后,你对后续任何一篇文章中涉及的模块,都能快速定位到它在整体架构中的位置。

从仓库顶层看分界

在进入框架内部之前,先看一个最关键的组织决策:go-zero 仓库包含了两个独立的 Go module

两个 module,两种用途

打开仓库根目录的 go.mod,第一行是:

1
module github.com/zeromicro/go-zero

再打开 tools/goctl/go.mod,第一行是:

1
module github.com/zeromicro/go-zero/tools/goctl

这不是一个形式上的拆分——两个 module 有本质区别:

  • 根 modulegithub.com/zeromicro/go-zero):你编写的服务在编译和运行时直接依赖的代码。它包含 REST 框架、RPC 框架、配置加载、熔断器、服务发现等高并发生产环境下运行时所需的全部能力。依赖列表中包括了 etcd client、gRPC、OpenTelemetry、Prometheus client、Redis、PostgreSQL driver、MongoDB driver 等"重量级"组件。

  • goctl modulegithub.com/zeromicro/go-zero/tools/goctl):只在开发阶段使用的命令行工具。它依赖 ANTLR(语法解析器)、Cobra(CLI 框架)、protobuf 等工具链组件。这个 module 编译出的 goctl 二进制不包含你的业务代码也不会被你的服务 import——它只读定义文件、解析 AST、按模板生成 Go 代码。

这里有一个容易被忽略但非常重要的依赖关系:goctl module 依赖根 module。注意 tools/goctl/go.mod 这一行:

1
2
3
4
require (
github.com/zeromicro/go-zero v1.10.3
// ...
)

这不是循环依赖——goctl 生成代码时需要引用根框架的类型定义(比如生成的 config 结构体要嵌入 rest.RestConf)。但你的服务代码不需要依赖 goctl。这个单向依赖保证了工具和框架之间的版本同步:你用 goctl@v1.10.3 生成的代码,一定与 go-zero@v1.10.3 的框架 API 匹配。

顶层目录速览

忽略测试辅助目录后,go-zero 仓库的骨架是这样的:

1
2
3
4
5
6
7
8
9
go-zero/
├── core/ # 核心能力层:配置、日志、熔断、限流、并发原语、数据访问
├── rest/ # REST 框架:Server、Router、参数解析、中间件链、响应写入
├── zrpc/ # RPC 框架:服务端、客户端、服务发现、负载均衡、拦截器
├── gateway/ # API 网关:HTTP → gRPC 协议转换,基于 REST Server 构建
├── mcp/ # MCP 协议支持:将服务能力暴露为 AI 可调用的 Tool
├── internal/ # 跨模块共享的内部实现:健康检查、dev server、profiling
├── tools/goctl/ # 代码生成工具链(独立 Go module)
└── go.mod # 根 module 定义

从这个目录结构就能看出 go-zero 的核心设计思路:core 是共享底座,restzrpc 是两大协议框架,gatewaymcp 是在底座上加盖的"上层建筑"。下一节我们把这六个部分纳入一个更结构化的分层视角。

四层架构

从依赖关系和抽象层次出发,go-zero 的代码可以被组织为四层。每一层有明确的职责边界,且依赖方向总是"上层依赖下层"。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌─────────────────────────────────────────────────────────┐
│ 工具层 (goctl) │
│ tools/goctl/api/ tools/goctl/rpc/ tools/goctl/model/ │
│ 职责:把定义文件解析为 AST,按模板生成 Go 代码 │
└──────────────────────┬──────────────────────────────────┘
生成代码调用框架 API (不在运行时执行)
┌──────────────────────▼──────────────────────────────────┐
│ 框架层 (REST / zRPC / Gateway / MCP) │
│ rest/ zrpc/ gateway/ mcp/ │
│ 职责:HTTP/RPC 协议处理、路由、参数绑定、中间件/拦截器 │
└──────────────────────┬──────────────────────────────────┘
import
┌──────────────────────▼──────────────────────────────────┐
│ 核心能力层 (core/) │
│ conf logx breaker load limit discov syncx │
│ stores collection stat trace prometheus threading │
│ 职责:配置、弹性、并发、数据访问、可观测性等共享底座 │
└──────────────────────┬──────────────────────────────────┘
import (仅限框架内部)
┌──────────────────────▼──────────────────────────────────┐
│ 内部实现层 (internal/) │
│ health devserver profiling trace encoding │
│ 职责:跨模块共享但不应对外开放的实现细节 │
└─────────────────────────────────────────────────────────┘

下面我们逐层展开。

核心能力层:core/——所有上层能力的共享底座

core/ 是 go-zero 最大的目录,包含 40 多个子包。这些包可以被 rest/zrpc/gateway/mcp/ 自由引用,但它们之间互相独立,不存在循环依赖。按照功能领域,可以归为五组:

配置与日志

职责
core/conf 配置文件的加载与映射。MustLoad 读取 YAML/JSON/TOML 文件,通过 struct tag 驱动默认值填充和环境变量覆盖。
core/logx 上下文 Logger,支持 JSON 编码、文件轮转、按级别过滤、带 trace 信息的 WithContext。
core/logc logx 的精简封装,提供 logc.Infof(ctx, ...) 式的调用,让业务代码写日志更简洁。
core/mapping 通用的"map → struct"映射引擎。rest/httpx 用它把 path/query/form/header 参数映射到请求结构体,core/conf 用它把 YAML map 映射到配置结构体。

弹性保护

职责
core/breaker Google SRE 熔断算法实现。提供 Breaker 接口和基于滑动窗口的熔断策略。
core/load 自适应降载(BBR 思路),根据 CPU 使用率、请求延时和并发量决定是否丢弃请求。
core/limit 基于 Redis 的固定窗口和令牌桶限流器。

数据访问与缓存

职责
core/stores/sqlx MySQL 数据库访问层,提供连接管理、事务、bulk insert、慢查询日志和 trace 集成。
core/stores/redis Redis 客户端封装,支持单机和 cluster 模式,提供 hook、分布式锁和 Lua 脚本缓存。
core/stores/mon MongoDB 访问封装。
core/stores/cache 缓存一致性框架。CacheNode 实现了 Cache-Aside 模式:读时先查缓存,未命中则回源 DB 并回填;写时先更新 DB 再删除缓存。配合占位值防穿透、SingleFlight 防击穿、随机过期防雪崩。
core/stores/sqlc 带缓存的 MySQL 访问。在 sqlx 基础上集成 cache,自动处理读写时的缓存一致性。
core/stores/postgres PostgreSQL 访问,与 sqlx 类似的设计。

并发与集合

职责
core/syncx 高级同步原语:Pool(对象池)、Limit(本地限并发)、SingleFlight(请求合并)、Barrier(栅栏)、SpinLock(自旋锁)。
core/threading 并发执行辅助:RoutineGroup(goroutine 组)、RunSafe(带 recover 的 goroutine)。
core/collection 高性能数据结构:RollingWindow(滑动窗口,熔断/降载的数据来源)、TimingWheel(时间轮,超时/延迟任务调度)。
core/mr MapReduce 框架,支持并行 map 和串行/并行 reduce,带 context 取消和错误传播。
core/fx 函数式编程辅助,流式处理管道。

基础设施

职责
core/service 服务生命周期管理。ServiceConf 定义服务的通用配置,ServiceGroup 管理多服务的协同启停。
core/proc 进程优雅退出。监听 OS 信号,依次调用 wrapUp listeners → sleep → shutdown listeners → 超时强制退出。
core/discov 服务发现抽象。publisher 将服务注册到 etcd,subscriber 通过 etcd watch 实时获取实例变更。
core/stat 运行时指标收集。Metrics 记录 QPS 和延迟分位值,RemoteWriter 将指标上报到远程。
core/metric 更细粒度的指标定义和 Counter/Vec 抽象。
core/trace OpenTelemetry 集成。Agent 封装了 trace exporter、propagator、sampler 的初始化和停止。
core/prometheus Prometheus 指标采集启动器。
core/queue 消息队列抽象,支持多分区消费者组。

这些包都定位为"纯机制"——它们不关心 REST 还是 RPC,不关心 http.Request 还是 protobuf 消息。这种与协议无关的设计,使得 rest/zrpc/ 可以复用同一套熔断器、同一套服务发现、同一个 ServiceGroup

框架层:rest/zrpc/gateway/mcp/——四种服务形态

如果说 core/ 提供的是"砖块",框架层就是用这些砖砌成的"建筑"。

REST 框架:rest/

rest/ 是 go-zero 中最完整的框架实现。它对外暴露的核心 API 可以浓缩为三个类型和一个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 配置——嵌入 ServiceConf,叠加 HTTP 特有字段
type RestConf struct {
service.ServiceConf
Host string `json:",default=0.0.0.0"`
Port int
MaxConns int `json:",default=10000"`
MaxBytes int64 `json:",default=1048576"`
Timeout int64 `json:",default=3000"`
// ...
}

// 服务器——组合 engine 和 router
type Server struct {
ngin *engine
router httpx.Router
}

// 创建
func NewServer(c RestConf, opts ...RunOption) (*Server, error)

Server 不直接处理 HTTP,而是持有 enginerouter

  • engine:负责配置、中间件链的构建和 HTTP 服务器的实际启动。它不关心路由是用哪种算法匹配的,只关心"把路由绑定到某个 Router 上"。
  • router:实现了 httpx.Router 接口,基于 radix tree 做路径匹配和变量提取。

这种"服务器 = 配置与生命周期管理(engine)+ 路由匹配(router)"的拆分,使得你可以通过 WithRouter 这个 RunOption 替换底层路由实现,而不影响中间件链和启动流程。

REST 框架内部的子包关系如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
rest/
├── server.go # Server 公开 API + RunOption
├── config.go # RestConf、MiddlewaresConf
├── engine.go # engine 内部:路由绑定、中间件链构建、HTTP 启动
├── types.go # Route、Middleware、RouteOption
├── chain/chain.go # 中间件链抽象(Chain 接口)
├── router/ # 路由匹配实现(patRouter,基于 radix tree)
├── httpx/ # HTTP 参数解析(Parse)与响应写入(OkJson/Error)
├── handler/ # 22 个中间件 handler(trace、breaker、shedding...)
├── pathvar/ # 路径变量提取与注入
├── httpc/ # HTTP 客户端工具
├── token/ # JWT 相关工具
└── internal/ # REST 专属的内部实现(cors、encoding、starter...)

注意到 rest/internal/ 和根目录的 internal/ 不是一回事——前者的包只能被 rest/ 及其子包 import,后者的包可以被 rest/zrpc/ 等多个框架模块共用。两者都是 internal 包,但作用域不同。

RPC 框架:zrpc/

zrpc/ 的结构与 rest/ 对仗工整,但它并非从零编写,而是在标准 gRPC 之上叠加 go-zero 的生产能力

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 服务端配置
type RpcServerConf struct {
service.ServiceConf
ListenOn string
Etcd discov.EtcdConf `json:",optional,inherit"`
Timeout int64 `json:",default=2000"`
CpuThreshold int64 `json:",default=900,range=[0:1000)"`
Health bool `json:",default=true"`
Middlewares ServerMiddlewaresConf
}

// 客户端配置
type RpcClientConf struct {
Etcd discov.EtcdConf `json:",optional,inherit"`
Endpoints []string `json:",optional"`
Target string `json:",optional"`
Timeout int64 `json:",default=2000"`
BalancerName string `json:",default=p2c_ewma"`
// ...
}

RpcServerConf 同样嵌入 ServiceConf,这意味着启动一个 RPC 服务时,日志、Prometheus、链路追踪和优雅退出这些基础设施同样会被自动初始化——与 REST 服务完全一致。

zrpc/ 的目录结构比 rest/ 更紧凑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
zrpc/
├── server.go # RpcServer 公开 API
├── client.go # RpcClient 公开 API
├── config.go # RpcServerConf、RpcClientConf
├── resolver/ # gRPC name resolver:直连、etcd、Kubernetes
└── internal/
├── server.go # Server 接口(内部抽象)
├── rpcserver.go # 标准 RPC server 实现
├── rpcpubserver.go # 带 etcd 注册的 RPC server 实现
├── client.go # 客户端实现
├── auth/ # 服务间认证
├── serverinterceptors/ # 服务端拦截器(trace/breaker/shedding/...)
├── clientinterceptors/ # 客户端拦截器(trace/timeout/breaker/...)
├── balancer/
│ ├── p2c/ # P2C + EWMA 负载均衡
│ └── consistenthash/ # 一致性哈希
└── codes/ # gRPC 状态码处理

rest/internal/ 相比,zrpc/internal/ 集中了更多 RPC 专属实现:除了服务端和客户端的底层封装,还包括认证、拦截器和负载均衡器等。P2C 放在 zrpc/internal/balancer/ 下,恰恰是因为它只服务于 zrpc/,且不需要作为公共 API 暴露。按照 Go 的 internal 包规则,zrpc/internal/ 只能被 zrpc/ 目录树中的代码导入;根目录的 internal/ 则可以供 rest/zrpc/ 等共同使用。因此,跨框架复用的内部能力放在根 internal/,RPC 专属能力则放在 zrpc/internal/

两种上层建筑:gateway/mcp/

如果你理解了 REST 和 zRPC 的拆分,那么 gateway/mcp/ 的定位就很直观了——它们都是在现有框架上复用而非重写的上层建筑:

  • gateway/:一个 API 网关。它的 Server 嵌入了 *rest.Server——这意味着网关本身就是一个 REST 服务,只不过它的 handler 不调用本地 logic,而是把 HTTP 请求转换为 gRPC 调用转发到下游。于是网关天然继承了 REST 的中间件链、限流、熔断和优雅退出。

  • mcp/:将服务能力暴露为 Model Context Protocol 的 Tool。它的内部也创建了一个 *rest.Server 作为 HTTP 传输层——MCP 的 SSE 和 Streamable HTTP 两种传输模式都复用了 REST 的 HTTP 服务器能力。

这种 组合优先、而非继承 的复用方式,是理解 go-zero 框架层设计的关键

内部实现层:internal/——跨模块共享但不对外承诺

根目录的 internal/ 包是 Go 语言 只对内可见 机制的刻意应用:

1
2
3
4
5
6
7
internal/
├── health/ # 健康检查探针管理器
├── devserver/ # 开发服务器(提供调试端点)
├── profiling/ # 持续 profiling(Pyroscope)
├── encoding/ # 编码/解码工具
├── mock/ # mock proto 文件
└── trace/ # trace 相关辅助

internal/ 中每个子包都是"应该共享"又"不应对用户暴露":

  • health 同时被 REST 的 starter 和 zRPC 的健康检查机制使用,但对用户而言,它只是一个实现细节——用户只需要在配置中写 Health: true
  • devserver 为 REST 和 RPC 服务统一提供 pprof 端点、健康检查和指标查询的 HTTP 路由,但它只在开发/调试环境中启用。
  • profiling 封装了 Pyroscope 持续 profiling 的启动和停止,同样是"REST 和 RPC 都需要,但用户不需要直接操作它的 API"。

工具层不在运行时

最后回顾工具层——tools/goctl/。它与运行时框架的唯一关联是:生成的代码 import 了框架的公开包。比如 goctl api new greet 生成的主入口会写:

1
2
3
4
import (
"github.com/zeromicro/go-zero/core/conf"
"github.com/zeromicro/go-zero/rest"
)

这种 生成代码调用框架 API 的衔接方式,使得 goctl 和框架可以独立发布、独立演化——只要你保持公开 API 的向后兼容,goctl 的升级不会影响已上线的服务,反之亦然。

一条 REST 请求穿过四层

有了四层结构的认知,我们再来看一次具体的调用——不是看单个函数做了什么(第一篇已经讲过),而是看调用链路如何串联不同层的模块。以 GET /from/you 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
main() greet.go

├─ conf.MustLoad(...) 【核心能力层:core/conf】

├─ rest.MustNewServer(c.RestConf) 【框架层:rest/server.go】
│ └─ c.SetUp() 【核心能力层:core/service】
│ ├─ logx.SetUp(...) 【核心能力层:core/logx】
│ ├─ prometheus.StartAgent(...) 【核心能力层:core/prometheus】
│ ├─ trace.StartAgent(...) 【核心能力层:core/trace】
│ ├─ proc.Setup(...) 【核心能力层:core/proc】
│ ├─ devserver.StartAgent(...) 【内部实现层:internal/devserver】
│ └─ profiling.Start(...) 【内部实现层:internal/profiling】
│ └─ newEngine(c) 【框架层:rest/engine.go】
│ └─ load.NewAdaptiveShedder(...) 【核心能力层:core/load】

├─ handler.RegisterHandlers(server, ctx) 【生成的 handler 代码】
│ └─ server.AddRoutes(...) 【框架层:rest/server.go】
│ └─ engine.addRoutes(...) 【框架层:rest/engine.go】

└─ server.Start() 【框架层:rest/server.go】
└─ engine.start(router) 【框架层:rest/engine.go】
├─ engine.bindRoutes(router) 【框架层:rest/engine.go】
│ └─ engine.buildChainWithNativeMiddlewares()
│ ├─ handler.TraceHandler 【框架层:rest/handler】
│ ├─ handler.LogHandler
│ ├─ handler.PrometheusHandler
│ ├─ handler.MaxConnsHandler 【核心能力层:core/syncx】
│ ├─ handler.BreakerHandler 【核心能力层:core/breaker】
│ ├─ handler.SheddingHandler 【核心能力层:core/load】
│ ├─ handler.TimeoutHandler
│ ├─ handler.RecoverHandler
│ ├─ handler.MetricHandler 【核心能力层:core/stat】
│ ├─ handler.MaxBytesHandler
│ └─ handler.GunzipHandler
└─ internal.StartHttp(...) 【框架层:rest/internal】
├─ healthManager.MarkReady() 【内部实现层:internal/health】
└─ svr.ListenAndServe()

── HTTP 请求到达 ──

router.ServeHTTP(w, r) 【框架层:rest/router】
└─ patRouter.ServeHTTP 【框架层:rest/router】
└─ tree.Search(reqPath) 【核心能力层:core/search】
└─ 匹配到 /from/:name

[中间件链依次执行]
├─ TraceHandler → LogHandler → PrometheusHandler
├─ MaxConnsHandler → BreakerHandler → SheddingHandler
├─ TimeoutHandler
├─ RecoverHandler
├─ MetricHandler → MaxBytesHandler → GunzipHandler
└─ GreetHandler(w, r) 【生成的 handler 代码】
├─ httpx.Parse(r, &req) 【框架层:rest/httpx】
│ ├─ pathvar.Vars(r) 【框架层:rest/pathvar】
│ └─ mapping.Unmarshaler 【核心能力层:core/mapping】
├─ logic.NewGreetLogic(ctx, svcCtx) 【业务代码:logic】
└─ httpx.OkJsonCtx(w, resp) 【框架层:rest/httpx】

这个调用链清晰地展示了四层架构的协作方式:框架层处理协议(HTTP/Router/Middleware),核心能力层提供机制(Conf/Log/Breaker/Load/Mapping),内部实现层支撑基础设施(Health/DevServer/Profiling),而工具层负责将这一切组织成可编译的工程代码

一条 RPC 请求穿过四层

现在看 RPC 侧。虽然 zRPC 的协议不同,但它穿过四层的方式与 REST 高度对称:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
main()

├─ zrpc.MustNewServer(c, register) 【框架层:zrpc/server.go】
│ ├─ c.Validate() 【框架层:zrpc/config.go】
│ ├─ internal.NewRpcPubServer(...) 【框架层:zrpc/internal】
│ │ └─ discov.NewPublisher(...) 【核心能力层:core/discov】
│ ├─ setupUnaryInterceptors(server, c) 【框架层:zrpc/server.go】
│ │ ├─ UnaryTracingInterceptor 【核心能力层:core/trace】
│ │ ├─ UnaryRecoverInterceptor
│ │ ├─ UnaryStatInterceptor 【核心能力层:core/stat】
│ │ ├─ UnaryPrometheusInterceptor 【核心能力层:core/prometheus】
│ │ ├─ UnaryBreakerInterceptor 【核心能力层:core/breaker】
│ │ ├─ UnarySheddingInterceptor 【核心能力层:core/load】
│ │ └─ UnaryTimeoutInterceptor
│ └─ c.SetUp() 【核心能力层:core/service】
│ └─ (同 REST——日志、trace、prometheus...)

└─ server.Start() 【框架层:zrpc/server.go】
└─ server.Start(register) 【框架层:zrpc/internal】
├─ publisher.Register(...) 【核心能力层:core/discov】
└─ grpcServer.Serve(listener) 【gRPC 标准库】

── RPC 请求到达 ──

[拦截器链依次执行]
├─ UnaryTracingInterceptor
├─ UnaryRecoverInterceptor
├─ UnaryStatInterceptor
├─ UnaryPrometheusInterceptor
├─ UnaryBreakerInterceptor
├─ UnarySheddingInterceptor
├─ UnaryTimeoutInterceptor
└─ 业务 gRPC handler

对比 REST 和 RPC 的调用链,你会发现核心能力层的复用程度惊人:core/breaker(熔断)、core/load(降载)、core/stat(指标)、core/trace(链路追踪)、core/prometheus(Prometheus)、core/service(配置与优雅退出)——这六个核心包在 REST 和 RPC 两侧被完全相同的方式集成。区别只在于封装方式:REST 侧用中间件函数(func(http.Handler) http.Handler),RPC 侧用 gRPC 拦截器(grpc.UnaryServerInterceptor)。

贯穿各层的三种设计模式

看完 REST 和 RPC 的调用链,你可能已经注意到一些反复出现的代码形态。go-zero 中有三种设计模式贯穿几乎所有模块,理解它们是阅读任何源码的前提。

配置组合:嵌入而非继承

go-zero 的配置结构体从不使用深层继承,而是通过 struct 嵌入来实现配置的组合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// core/service/serviceconf.go
type ServiceConf struct {
Name string
Log logx.LogConf
Mode string `json:",default=pro"`
Telemetry trace.Config
DevServer DevServerConfig
Shutdown proc.ShutdownConf
Profiling profiling.Config
}

// rest/config.go
type RestConf struct {
service.ServiceConf // ← 嵌入
Host string `json:",default=0.0.0.0"`
Port int
MaxConns int `json:",default=10000"`
// ...
}

// zrpc/config.go
type RpcServerConf struct {
service.ServiceConf // ← 嵌入
ListenOn string
Etcd discov.EtcdConf
Timeout int64 `json:",default=2000"`
// ...
}

这带来两个关键好处:

  • 一次配置,全局生效ServiceConf.SetUp() 中的日志、trace、prometheus 等初始化逻辑,对 REST 和 RPC 完全相同——因为它们都通过嵌入 ServiceConf 获得了这些字段和 SetUp() 方法。
  • 同一进程可以混合使用:如果你需要同时启动 REST 和 RPC 服务,配置结构体可以这样写:
1
2
3
4
type Config struct {
rest.RestConf
zrpc.RpcServerConf
}

两个嵌入的结构体共享同一个 ServiceConf 的设施(只初始化一次),各自管理自己的协议层配置。

函数选项:灵活且可扩展

go-zero 大量使用 functional options 模式来定制行为。这种模式的核心是:不定义复杂的配置结构体,而是让调用方传入零或多个函数来修改服务器的内部状态

在 REST 侧,RunOptionRouteOption 分别在两个粒度上工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 服务器级别的选项:在 MustNewServer / NewServer 时生效
type RunOption func(*Server)

func WithCors(origin ...string) RunOption
func WithNotFoundHandler(handler http.Handler) RunOption
func WithChain(chn chain.Chain) RunOption
func WithTLSConfig(cfg *tls.Config) RunOption
func WithRouter(router httpx.Router) RunOption

// 路由级别的选项:在 AddRoutes 时生效
type RouteOption func(r *featuredRoutes)

func WithJwt(secret string) RouteOption
func WithSignature(signature SignatureConf) RouteOption
func WithTimeout(timeout time.Duration) RouteOption
func WithMaxBytes(maxBytes int64) RouteOption
func WithPrefix(group string) RouteOption
func WithSSE() RouteOption

使用示例:

1
2
3
4
5
6
7
8
9
10
server := rest.MustNewServer(c.RestConf,
rest.WithCors("https://example.com"),
rest.WithNotFoundHandler(customNotFound),
)

server.AddRoutes(
[]rest.Route{...},
rest.WithJwt("my-secret"),
rest.WithTimeout(5*time.Second),
)

RunOptionRouteOption 的分层很精妙:前者影响整个服务器的行为(CORS、TLS、not found handler),后者只影响特定路由组(JWT、签名、超时)。这避免了 全局配置覆盖路由配置路由配置需要重复全局信息 的两难。

在 RPC 侧,对应的是 ClientOption(调用 NewClient 时使用)和 gRPC 标准库的 grpc.ServerOption(调用 AddOptions 时使用)——模式一致,只是术语不同。

中间件链:洋葱模型的不可变链

go-zero 的 REST 中间件链实现是一个修改版的 Alice,核心类型定义在 rest/chain/chain.go

1
2
3
4
5
6
7
8
type Chain interface {
Append(middlewares ...Middleware) Chain
Prepend(middlewares ...Middleware) Chain
Then(h http.Handler) http.Handler
ThenFunc(fn http.HandlerFunc) http.Handler
}

type Middleware func(http.Handler) http.Handler

这个设计有三个特点:

不可变性。 AppendPrepend 返回新的 Chain 实例,不会修改原实例。这意味着同一个基础 chain 可以被多个路由安全复用——每个路由调用 Then 时得到的是独立的中间件实例。

洋葱模型。 调用 chain.New(m1, m2).Then(h) 等价于 m1(m2(h))。请求进入时先经过 m1、再经过 m2、最后到达 h;响应返回时则反向穿过——先经过 m2、再经过 m1。这正是标准 HTTP 中间件的执行方式。

类型转换。 REST 的 Middleware 类型是 func(next http.HandlerFunc) http.HandlerFunc,与 chain 的 func(http.Handler) http.Handler 不完全一致。engine 中的 convertMiddleware 负责桥接这两种签名:

1
2
3
4
5
func convertMiddleware(ware Middleware) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return ware(next.ServeHTTP)
}
}

在 RPC 侧,对等设计是 gRPC 拦截器链。UnaryServerInterceptorStreamServerInterceptor 本身就是"洋葱"——传入 handler,返回包装后的 handler。setupUnaryInterceptors 通过多次 AddUnaryInterceptors 以固定顺序叠加它们。

公开包与 internal 包的边界设计

最后我们梳理一下 go-zero 的可见性边界。在任何一个 Go 项目中,"什么包能 import 什么包"直接影响代码的组织方式和读者的学习路径。

三层边界

go-zero 中有三道可见性边界:

第一道:module 边界。 github.com/zeromicro/go-zerogithub.com/zeromicro/go-zero/tools/goctl 是两个独立 module。前者可以被任何项目的 go.mod 引用,后者通常通过 go install 安装为命令行工具。

第二道:公开 / internal 边界。 根 module 内的 internal/ 树只能被根 module 内部的包 import。具体来说:

  • internal/devservercore/service/serviceconf.go import——它通过 ServiceConf.SetUp() 被集成到服务生命周期中。
  • internal/healthrest/internal/starter.gozrpc/internal/rpcserver.go 引用——REST 和 RPC 的服务启动都共用同一个健康检查机制。
  • internal/profiling 同样在 ServiceConf.SetUp() 中被启用。

这些 internal/ 下的包有一个共同特征:如果把它们变成公开包,用户可以直接 import,但这不仅没有必要,而且会让 API 表面积翻倍。比如你永远不需要在自己的代码里直接操作 health.HealthManager——你只需要在配置中写 Health: true

第三道:模块内 internal 边界。 rest/internal/zrpc/internal/ 的包各自只能被自己的模块 import。例如:

  • rest/internal/cors 处理 CORS 头部,它是 WithCors 的实现依赖。
  • rest/internal/starter.go 封装了 http.Server 的创建和优雅关闭。
  • zrpc/internal/balancer/p2c 实现了 P2C 负载均衡算法。

这些包的粒度比根 internal/ 更细——它们不是"所有框架模块都需要",而是"只属于 REST 或只属于 zRPC"。

一个实用准则

当你在源码中追踪某个类型的定义时,可以从它的 import 路径快速判断它的定位:

路径特征 含义 你能依赖它吗
core/ 开头 共享核心能力,REST 和 RPC 都可以用 ✅ 可以
rest/ 开头但不含 internal REST 框架的公开 API ✅ 可以
zrpc/ 开头但不含 internal RPC 框架的公开 API ✅ 可以
含有 /internal/ 实现细节,不对外承诺兼容性 ❌ 不建议
github.com/zeromicro/go-zero/tools/goctl/... 另一个 module ❌ 不建议在服务代码中 import

总结

这篇文章画出了 go-zero 的"源码地图"。现在回顾我们已经建立的三层认知:

  1. 四层架构:工具层(goctl)→ 框架层(rest/zrpc/gateway/mcp)→ 核心能力层(core/)→ 内部实现层(internal/)。依赖方向自上而下,每层有明确的职责边界。

  2. REST 和 RPC 的对称设计:虽然协议不同——一个走 HTTP 中间件,一个走 gRPC 拦截器——但它们共享同一套核心能力层。ServiceConf 的嵌入使得日志、trace、弹性保护和服务发现"一次配置,两端生效"。

  3. 三种设计模式贯穿始终:配置组合(struct 嵌入)、函数选项(RunOption / RouteOption)和不可变中间件链(chain.Chain)。掌握了这三种模式,你就可以自信地阅读框架层任何模块的源码——代码形态是一致的。

有了这张地图,从下一篇开始我们就可以逐步深入各个模块的具体实现。下一篇主题是用 goctl.api 文件展开为可运行的工程——你将看到解析器如何把 DSL 文本转化为 AST、生成器如何把 AST 映射为 Go 代码,以及框架为什么用"生成代码调用公开 API"而不是"反射 + 运行时注册"来衔接工具层和运行时。