架构总览
两条主线:纵向是 verticle.Run 的启动序列,横向是
cmd → internal → pkg/core 的依赖方向。前者决定服务怎么活起来,
后者决定代码往哪放。
分层与依赖方向
flowchart TD
subgraph bin["可执行文件"]
CMD["cmd/<service>
main 只做一件事:verticle.Run"]
end
subgraph internal["internal/ — 业务代码"]
APP["app/
实现 verticle.Service
装配 domain"]
DOM["domain/<name>/
handler.go · service.go · dao.go"]
end
subgraph core["pkg/core/ — 基础设施(唯一入口)"]
V["verticle · app"]
H["httpx · errs · obs · config"]
D["db · redis · memcache · kafka"]
R["auth · ratelimit · breaker · retry
idempotency · cache · cron"]
end
DEPS["第三方驱动
pgx · go-redis · franz-go
rpcx · grpc"]
CMD --> APP
APP --> DOM
APP --> V
DOM --> H
DOM --> D
DOM --> R
D -.-> DEPS
classDef guard fill:#fdf6f0,stroke:#c8641f,color:#2b2722
class core guard
internal/)只能经 pkg/core/* 触碰第三方驱动:
实线是允许的调用方向,虚线只存在于 pkg/core 内部。
这条规则由 depguard 强制,不靠 review 提醒。
每个目录的职责
| 路径 | 职责 |
|---|---|
cmd/<service>/ |
一个二进制一个部署单元;main 只调 verticle.Run。 |
internal/app/ |
实现 verticle.Service,把基础设施句柄装配进 domain。 |
internal/domain/<name>/ |
一个业务域三件套:handler.go(HTTP 边界)、
service.go(业务)、dao.go(数据)。
|
pkg/core/* |
基础设施。业务只准从这里进(depguard 强制),第三方驱动被关在里面。 |
configs/ |
框架段 + 每服务一段,段 key 等于 Service.Name()。 |
migrations/ |
按文件名序执行,只追加不修改已有文件。 |
examples/<feature>/ |
每个能力一个可运行示例,离线可跑、带测试——所以 CI 能验证它们。 |
skills/ |
给 AI 的接入规范,与 AGENTS.md 一起充当"生成器"。 |
启动序列
verticle.Run 的每一步都是 fail-fast:任何一步返回错误,启动就停在那里,
不会带着半初始化状态开始服务。
flowchart TD
A["config.LoadMap(path)"] --> B["框架段 ParseMap + 服务段
按 Service.Name() 取"]
B --> C["obs.Init
日志先行,后面每步都有日志"]
C --> D{"trace 段配了吗"}
D -->|配了| D1["obs.InitTracing
OTLP exporter"]
D -->|没配| D2["no-op,零开销"]
D1 --> E["未知框架键告警
typo 不再静默"]
D2 --> E
E --> F["feature.Configure
灌入 feature: 段"]
F --> G{"各基础设施段"}
G --> G1["db · redis · memcache · kafka · auth
配了才建,未配为 nil"]
G1 --> H["svc.Setup(ctx, env)
解码服务段、装配 domain"]
H --> I["httpx.New + svc.BuildRouter
/healthz /readyz /metrics /version"]
I --> J{"实现了 Registrar 吗"}
J -->|GRPCRegistrar| J1["监听 grpc.addr"]
J -->|RPCXRegistrar| J2["监听 rpcx.addr"]
J -->|都没有| J3["告警且不监听
避免空端口被探活误判"]
J1 --> K["运行"]
J2 --> K
J3 --> K
K --> L["信号 / ctx 取消"]
L --> M["优雅退出:停 Block → 停 HTTP 排空
→ flush trace → 关基础设施"]
classDef decision fill:#f5f2ee,stroke:#8a8378,color:#2b2722
class D,G,J decision
优雅退出的顺序
// 顺序(app 包保证):
// 1. 停止 Block 里注册的后台任务(定时任务、消费者循环)
// 2. 关闭 HTTP server,排空在途请求
// 3. flush 链路追踪的 span
// 4. 逆序执行 AddCloser 注册的清理函数,最后关基础设施连接
env.Block("cron", s.sched.Run) // 随 ctx 取消而停
env.AddCloser(func() { flushMetrics() }) // 逆序执行,后注册的先跑
顺序是有理由的:后台任务先停,才不会在 HTTP 排空期间继续产生新工作; 基础设施最后关,才能在排空前把请求真正做完。
请求的生命周期
HTTP 侧的每一层也是显式的。两个中间件槽的区别值得记住:
Use 走封套(能返回 errs 短路,不碰
ResponseWriter),UseOuter 在 mux 外层
(能改响应头、覆盖 /healthz 与 HandleRaw)。
sequenceDiagram
autonumber
participant C as 客户端
participant G as guard
panic 恢复 · 体积限制
participant S as SecureHeaders
默认安全响应头
participant O as UseOuter 中间件
CORS · gzip · RealIP
participant M as mux
participant W as wrap
trace id · 超时 · 封套
participant MW as Use 中间件
认证 · 限流 · 幂等
participant H as Handler
C->>G: 请求
G->>S: 放行
S->>O: 安全头已就位
O->>M: 路由匹配
M->>W: 命中注册的 handler
W->>W: 开 server span,定 X-Request-ID
W->>MW: ctx(带 trace id + 超时)
MW->>H: ctx + *http.Request
H-->>MW: (any, error)
MW-->>W: 结果
W->>W: 渲染封套 · 记访问日志与指标
W-->>C: 响应
context.WithoutCancel 派生。这样客户端断连不会被报成服务端超时,
而 handler 还能把副作用(以及幂等结果)完整写下去。
一致性契约
四条约定,前两条被 lint 强制,后两条被测试与例子的存在形态保证。
| 约定 | 强制方式 |
|---|---|
| 业务代码不 import 驱动库 | depguard internal-no-drivers / examples-no-drivers |
| 业务代码不 import Web 框架 | depguard no-web-frameworks |
| handler 签名统一 | 类型系统——srv.Handle 只接受 HandlerFunc |
| 每个能力有可运行的离线示例 + 测试 | make ci 会跑 ./examples/...,示例坏了 CI 就红 |