目录与约定
布局不是审美问题:它决定了新代码该写在哪、依赖往哪个方向走、以及哪些规则可以交给 工具去拦。这页是目录表与四条硬约定的索引。
目录布局
| 路径 | 放什么 | 不放什么 |
|---|---|---|
cmd/<service>/ |
一个二进制一个部署单元。当前是 cmd/demo/main.go:解析 -conf,调一次 verticle.Run |
业务逻辑、装配代码 |
cmd/migrate/ |
迁移工具:-driver / -dsn / -dir,把 migrations/*.sql 应用到库里 |
— |
internal/app/ |
实现 verticle.Service:Name() / Setup() / BuildRouter(),解码服务段、从 Env 取依赖、构造 domain、挂 Block 后台任务 |
业务规则(属于 domain) |
internal/domain/<name>/ |
业务域三件套:handler.go(HTTP 边界:解析入参 → 调 service → 返回 payload 或 errs)、service.go(纯业务逻辑,依赖经 Deps 注入)、dao.go(数据访问,SQL 与行结构) |
路由注册、封套/状态码/中间件等框架关注点 |
pkg/core/ |
可复用的基础设施,每个包单一职责:config / db / redis / memcache / kafka / httpx / obs / errs / verticle / app / auth / ratelimit / breaker / retry / idempotency / cache / cron / feature / audit / llm … |
业务语义;也不 import internal/ |
configs/ |
三份配置:config.min.yaml(零依赖,只有 http + log + 服务段)、config.sqlite.yaml(带纯 Go SQLite)、config.yaml(含 db/redis/memcache/kafka 段) |
密钥明文——用 ${VAR:default} 从环境变量取 |
migrations/*.sql |
数据库迁移,按文件名序执行 | 修改已提交的迁移文件——只追加 |
examples/<feature>/ |
每个能力一个可运行示例:main.go + README.md + 测试(+ 需要 HTTP 形态时带 config.yaml) |
只给片段不给跑法的"伪示例" |
skills/ |
AI skill,matex- 前缀,每个能力一个目录(SKILL.md) |
— |
.claude-plugin/ |
skill 插件清单:plugin.json(matex-skills 大伞)与 marketplace.json |
— |
docs/ |
本站静态站点源码与 design.md(逐项取舍说明) |
— |
依赖方向
flowchart TD
CMD["cmd/<service>
main:只调 verticle.Run"]
APP["internal/app/
实现 verticle.Service
装配 domain"]
DOM["internal/domain/<name>/
handler · service · dao"]
CORE["pkg/core/*
唯一的基础设施入口"]
DRV["第三方驱动
pgx · go-sql-driver · modernc-sqlite
go-redis · gomemcache · franz-go · grpc · rpcx"]
CMD --> APP
APP --> DOM
APP --> CORE
DOM --> CORE
CORE -.-> DRV
DOM -. "禁止(depguard 拦截)" .-> DRV
classDef core fill:#fdf6f0,stroke:#c8641f,color:#2b2722
class CORE core
classDef bad fill:#fff,stroke:#999,stroke-dasharray: 4 3,color:#777
class DRV bad
pkg/core 内部的驱动依赖。
domain 到驱动的直连被 depguard 拦住——它不是靠 review 记住的纪律,是构建期错误。
四条强制约定
"约定"意味着违反时应该有东西报错。下面四条分别由 .golangci.yml、
类型签名与 make ci 承担。
1. 业务代码不 import 驱动库
单一基础设施入口。业务要用数据库就经 pkg/core/db,要 Redis 就经
pkg/core/redis——换驱动只改 pkg/core,业务一行不动。
business-no-drivers 规则覆盖 **/internal/**,
examples-no-drivers 覆盖示例(示例要示范正确写法):
business-no-drivers:
files:
- "**/internal/**"
deny:
- pkg: github.com/jackc/pgx
desc: use pkg/core/db
- pkg: github.com/go-sql-driver/mysql
desc: use pkg/core/db
- pkg: modernc.org/sqlite
desc: use pkg/core/db
- pkg: github.com/redis/go-redis
desc: use pkg/core/redis
- pkg: github.com/bradfitz/gomemcache
desc: use pkg/core/memcache
- pkg: github.com/twmb/franz-go
desc: use pkg/core/kafka
franz-go/pkg/kfake 跑进程内 broker,
让 Kafka 用例离线可跑。kafka-test-fake-broker 规则把豁免收窄到
测试文件,非测试代码仍不得 import franz-go。
2. 不用 Web 框架
路由与中间件用标准库:Go 1.22 的 ServeMux 原生支持方法路由与路径参数,
pkg/core/httpx 补齐统一封套、超时、trace、访问日志、指标与错误映射。
再引一个第三方路由只会多一份依赖面和一套分叉语义。no-web-framework
规则对 所有 Go 文件生效:
no-web-framework:
files:
- "**/*.go"
deny:
- pkg: github.com/gin-gonic/gin
desc: matex uses net/http via pkg/core/httpx
- pkg: github.com/labstack/echo
desc: matex uses net/http via pkg/core/httpx
- pkg: github.com/gofiber/fiber
desc: matex uses net/http via pkg/core/httpx
- pkg: github.com/go-chi/chi
desc: matex uses net/http via pkg/core/httpx
- pkg: github.com/gorilla/mux
desc: matex uses net/http via pkg/core/httpx
3. handler 签名固定
func(ctx context.Context, r *http.Request) (any, error)
handler 不碰 http.ResponseWriter:返回 nil 即 204,
返回 errs.* 即对应状态码加 {code,msg,data} 封套。
签名固定换来三件事:handler 能直接单测(不需要构造响应记录器以外的任何东西)、
能被中间件统一包装、能统一加超时与日志。
需要原生控制(流式、自定义状态码、SSE、文件下载)时走 HandleRaw,
它是唯一的逃生口,且不经过封套。细节见 HTTP 与路由。
4. 每个能力都有离线可跑的示例
examples/<feature>/ 一个目录一个能力,必须能go run、
必须有测试,且离线可跑:SQLite 用纯 Go 驱动(modernc.org/sqlite,
无 cgo)、Redis 用 redistest(miniredis)、Kafka 用
kfake、gRPC/rpcx 用进程内 server。所以 make ci
就能验证全部示例,不需要 Docker、不需要网络。
make example NAME=http # 等价于 go run ./examples/http
make test-short # 含 sqlite / miniredis / kfake / 进程内 RPC 用例
make ci # vet + test-short + build,示例一并被验证
一个部署单元 = 一个 cmd 二进制
verticle.Service 是部署单元的边界,而 cmd/<service>/
是它的落地形态:一个 main 包对应一个 Name()、一个进程、
一份配置段、一套日志与指标标签。要加第二个部署单元,复制 cmd/demo
与 internal/app 改名即可——Name() 决定去配置文件的哪一段
取自己的配置,框架段(http、db…)与它无关。
// cmd/demo/main.go —— main 的全部内容
func main() {
conf := flag.String("conf", "", "config file (default configs/config.yaml or $CONFIG_FILE)")
flag.Parse()
if err := verticle.Run(context.Background(), &app.DemoService{}, verticle.WithConf(*conf)); err != nil {
fmt.Fprintln(os.Stderr, "fatal:", err)
os.Exit(1)
}
}
由此推出两条经常被问到的推论:不要在一个进程里塞两个服务(配置段、日志标签、 指标前缀都会打架,生命周期也会互相拖累);不要给不同环境准备不同的 main—— 依赖是否存在是部署事实,删掉配置里那一段即可,代码不用改。
verticle.Run 按配置初始化基础设施:配了 db: 段才有
env.DB(),没配就是 nil。一个只有 http: 段的最小配置也能起服务
(configs/config.min.yaml 就是),其余依赖随加随用。
新代码放哪
| 你要做的事 | 落到哪 |
|---|---|
| 加一个新接口 | internal/domain/<name>/handler.go + 在 BuildRouter 注册 |
| 加一条业务规则 | internal/domain/<name>/service.go |
| 加一条 SQL | internal/domain/<name>/dao.go;表结构改动写成 migrations/000N_*.sql |
| 加一个后台任务或消费者 | internal/app 的 Setup 里 env.Block(...) |
| 加一个横切能力(认证、限流…) | 优先看 pkg/core/ 是否已有;要新写就照该目录的包结构加,并补一个 example |
| 加一段业务配置 | 服务段结构体加 default:/env:/optional tag,在 configs/config.yaml 的服务段写值 |
下一步
- 命令与部署:Makefile 目标、CI 任务、Docker 镜像与本地依赖。
- pkg/core 速查:每个包的用途与开关。
- examples/http:按这套布局写出来的最小服务。