19 KiB
API 契约与项目生成
首次接入请先看 README:包含固定远程版本安装、可运行项目、完整 .api 示例和已有服务接入步骤。本页用于查阅语法与生成规则;下方带 --framework-dir 的命令用于框架源码开发。
本阶段目标:从 .api 定义生成可运行的 Go 项目;定义、绑定、校验与 OpenAPI 保持一致;重复生成不覆盖业务实现;业务代码继续独立于网络层。实现入口是 cmd/go-omni/apigen,运行时契约编译器是 contract。
与 go-zero 的对应关系
调研参考了 go-zero 官方的 API DSL、参数规则、goctl API 和 项目结构。借鉴其声明接口后生成项目的工作方式:统一的类型定义、路由声明、分组、前缀和命名中间件。实现独立编写,不引入 go-zero 或 Gin 运行时依赖。
| 使用体验 | go-omni 实现 |
|---|---|
| 新建接口项目 | go-omni api new <directory> |
| 按契约生成、更新 | go-omni api generate --api user.api --dir . |
| 熟悉的命令别名 | api go -api user.api -dir . |
| 校验契约 | api validate --api user.api |
| CI 检查生成产物是否过期 | api generate --api user.api --dir . --check |
| 业务逻辑目录 | internal/service/<group>,沿用 Omni 的 Service 分层 |
| 依赖装配 | internal/servicecontext |
| 接口文档 | 自动生成 docs/openapi.json,同时提供 /openapi.json |
这是明确限定的 go-zero 风格方言,不是完整 goctl 兼容实现。不支持的声明和校验标签会在生成前报错。
立即试用
源码开发时,从 go-omni 仓库运行,显式引用本地框架:
go run ./cmd/go-omni api new \
--module example.com/omniDemo --framework-dir . ../omniDemo
cd ../omniDemo
go mod tidy
go run ./cmd/server
访问 http://127.0.0.1:8080/api/greet/Alice 可得到带 data.message 的统一响应。api new 的问候示例包含可运行逻辑;自定义契约新生成的 Service 会明确返回 501,等待业务实现。
用现成的多分组定义生成项目,在 go-omni 仓库执行:
go run ./cmd/go-omni new --api examples/contracts/user.api \
--module example.com/userService --framework-dir . \
--log-output file --log-format json ../userService
api generate 在新目录中也会生成完整项目。默认只监听 localhost;GO_OMNI_HTTP_ADDR 修改地址,GO_OMNI_TRANSPORT=native 切换网络层。配置顺序为默认值 → etc/config.yaml → 环境变量。文件日志支持现有轮转配置。远程试用使用与 CLI 一致的固定版本,见 发布说明。
API 项目现在支持组合数据库、存储和日志:
go-omni new --api user.api --module example.com/userService \
--database mysql --storage local --log-output file ./userService
--database none|memory|mysql:API 默认none,memory是不创建数据库连接的别名;MySQL 生成连接池、配置、事务入口和 readiness 探针。--storage none|local|s3|r2|oss:默认 none,生成统一 Files / Uploads / Downloads / UploadLinks 依赖;本地签名对象端点为/storage/object。--log-output stdout|stderr|file与--log-format json|text:配置和无配置文件启动使用一致默认值。
初次选项记录在 .goOmni/apiManifest.json,以后直接 api generate 会继承。已有项目通过参数切换选项会报错,因为初始化代码属于业务方;可在新目录生成对照后手动迁移。--force 不允许覆盖 API 项目。旧式 CRUD 脚手架仍保留。
MySQL 密钥只从 GO_OMNI_DB_DSN 获取,非秘密池配置位于 YAML,环境变量优先。存储使用原有 GO_OMNI_STORAGE_*;API 项目不会额外生成公开的签名发放接口,鉴权和对象权限由业务实现。初始化失败会清理已打开资源,退出清理可重复调用。
HTTP 请求类型不能推导数据库表结构;迁移和 Repository 由业务自己维护。完整案例见 用户管理示例,覆盖真实 MySQL、事务审计、鉴权、文件导出和日志。
接口定义
syntax = "v1"
type UpdateUserReq {
UserID int64 `path:"userId,range=[1:1000000]"`
Notify bool `form:"notify,default=false"`
Name string `json:"name" binding:"required,max=50"`
Age *int `json:"age,optional,range=[0:150]"`
}
type UserResp {
UserID int64 `json:"userId"`
Name string `json:"name"`
}
@server (
group: user
prefix: /api/v1
)
service user-api {
@doc "Update a user"
@handler updateUser
put /users/:userId (UpdateUserReq) returns (UserResp)
}
类型和 Go 字段使用导出命名;JSON、query、path 名称使用 lowerCamelCase;Header 使用 HTTP 标准名称。响应类型定义业务返回值。未配置时使用默认公共体;当前源码支持在 .api 中声明公共响应模板,选择字段和结构,并按分组复用或由单接口覆盖,详见 公共响应体。
支持:
- 可省略的
syntax = "v1";info中的 title、version 和描述性元数据。 - 单独的
type X { ... }与type (...);基础标量、指针、嵌套命名类型、切片、字符串键 map。嵌套对象只使用 JSON 字段。 - 相对
.api导入及import (...),重复导入去重;拒绝循环和越过入口目录的导入。单文件 1 MiB、总计 8 MiB、最多 64 个文件,限制导入/类型深度及 schema 展开规模。 - 同一 service 名称的多个块;
@server的 group、prefix、middleware、auth、errors、response,group 可以写admin/user。 - 每个路由必须有
@handler;可选单行/多行@doc、@auth、@errors、@response;GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS。 - 可省略请求或响应;有响应默认 200,无响应默认 204。Omni 扩展
@status 201、@status 202;204 不允许响应类型,HEAD 不允许响应体,GET/HEAD 不允许 JSON 请求体。 - 每个请求字段只使用一个来源:
json、path、form、header。本版form明确表示 URL query,不解析 multipart 或表单 body。query 切片用重复键;header/path 使用标量。 - 来源标签中的
optional/omitempty、标量default、标量options=a|b、数值range=[1:100](支持开区间和单边界)。 binding的required、min、max、len、email。长度按 Unicode 字符或切片元素数;范围边界必须有限且位于 ±2^53 内。
暂不支持匿名嵌入、别名、递归类型、未声明的外部 Go 类型、自定义 JSON 编解码、[]byte、泛型、json:",string"、任意 validator 标签、go-zero 的 JWT/签名/SSE 声明,以及客户端 SDK 生成。认证验证通过声明关联的命名中间件实现。未列出的语法会明确报错。
业务错误、具名鉴权、多行文档及下述开发命令从 CLI v0.7.0-preview.1 提供。公共响应模板从 v0.8.0-preview 提供,CLI 与业务项目中的框架依赖需一起升级。
业务错误:声明、代码和文档共用一份定义
从 v0.10.0-preview 起,推荐用 errorCatalog 同时定义逻辑标识、对外业务码、提示和 HTTP 状态:
errorCatalog CommonErrors (
defaultStatus: 200
errors: {
"UsernameInvalid": {"key": "USERNAME_INVALID", "code": 400002, "message": "用户名格式不正确"},
"PasswordInvalid": {"key": "PASSWORD_INVALID", "code": 400002, "message": "密码格式不正确"},
"Forbidden": {"key": "FORBIDDEN", "status": 403, "code": 403001, "message": "没有权限"}
}
)
对象键是生成函数名;key 是唯一逻辑标识,允许项目自己的命名;code 是对外业务码,可以重复。数字码需搭配 codeType: int32 或 int64 的公共响应;字符串码搭配 string。目录不规定返回体字段。
省略 defaultStatus 时为 400;设为 200 时,只作用于目录里未单独写 status 的条目。生成路由自动绑定它实际声明的错误目录,在 App 中间件之前生效。业务只调用 apierrors.UsernameInvalid();无需再维护 codes / messages,也无需修改 main。手写路由可复用生成的 apierrors.Catalog()。
同一 HTTP 200 下,OpenAPI 用 anyOf 保留成功 DTO 和错误体,日志与指标仍识别业务失败。完整语法、路由作用域与手写项目配置见 统一错误目录,可运行契约见 errors.api。
原有声明继续支持:
error (
Unauthenticated 401 "UNAUTHENTICATED" "authentication required"
Forbidden 403 "FORBIDDEN" "permission denied"
UserNotFound 404 "USER_NOT_FOUND" "user not found"
EmailTaken 409 "EMAIL_TAKEN" "email already in use"
)
也可写单条 error EmailTaken 409 "EMAIL_TAKEN" "email already in use",或把错误目录放进相对导入的 errors.api。
名称用 UpperCamelCase,HTTP 状态允许 200–599 中可携带响应体的最终状态(排除 204/205/304),旧声明的逻辑错误码用大写字母/数字/下划线且全局唯一,message 是可公开返回的稳定提示。Catalog 为生成函数保留名称。
@server (errors: Unauthenticated) 声明组级错误;路由 @errors UserNotFound, EmailTaken 追加到组级列表并去重。
@errors none 清空继承的业务错误。未声明、重复声明或生成符号冲突会报告源码位置。
生成 internal/apierrors/errorsGen.go:
return nil, apierrors.EmailTaken()
// 可用 apierrors.CodeEmailTaken 引用同一个错误码。
每次调用返回新错误,避免并发修改共享错误对象。生成器不会自动把任意数据库错误转成业务错误;由 Service/适配层调用这些函数。 内部 Cause 可能进入访问日志,敏感驱动报错应像 userService 示例一样先归类再转换。
OpenAPI 对每个状态按选定的响应模板生成结构和命名 examples,同一 409 下多个错误不会相互覆盖;平铺模式的纯业务状态还列出 code enum。
默认框架错误状态与自动添加的鉴权 401 保留开放的 code 字段,容纳校验器/中间件实际错误。
x-omni-errors 提供错误目录与路由关联。目录解析已定义的错误,不自动推断数据库或其他未声明错误的业务含义,也不穷举所有运行时错误。
鉴权:声明与路由注册一起变化
auth UserAuth (
type: bearer
middleware: Auth
description: "Application access token"
)
@server (
group: user
prefix: /api/v1
auth: UserAuth
errors: Unauthenticated
)
service user-api {
@errors Forbidden, EmailTaken
@handler createUser
@status 201
post /users (CreateUserReq) returns (UserResp)
@auth none
@errors none
@handler publicStatus
get /status
}
auth 是具名契约,middleware 指向实际 hook;生成器把它放在普通 middleware 前面执行,并在注册任何路由前检查 hook 非空。
新 hook 未实现时返回 501,已有手写 hook 保留。需要在 hook 中验证凭证、失效时间、签发者、权限等;声明不会自动实现登录或 JWT 校验。
type: bearer:对应 HTTP Bearer,可选bearerFormat: JWT,这只是文档提示。type: apiKey:必须同时声明in: header或in: cookie和name,例如X-API-Key或sessionId。auth: UserAuth可在组级使用;路由@auth OtherAuth替换,@auth none撤销继承的声明鉴权。普通 middleware 继续保留。- 不要再把该 auth 的 hook 重复写进普通 middleware。不支持 query token、多个方案 AND/OR、OAuth2 scopes 或隐含认证策略。
- 被保护路由自动描述 401;403 和具体错误码由
@errors明确声明。未声明 auth 的旧项目仍按其原有 middleware 执行。
文档生成 components.securitySchemes 与逐接口 security;显式公共接口使用空数组。
结构遵循 OpenAPI 3.0.3 Security Requirement。
文档和日常开发命令
保留单行 @doc "摘要",也可以:
@doc (
summary: "Create a user"
description: "Writes user data and audit in the same transaction"
tags: users, administration
deprecated: false
)
未设置 tags 时使用 group 分组。info 的 description/desc 也会进入 OpenAPI;两者不能同时声明。
在生成项目目录内,不必重复输入 API 路径:
go-omni api validate # 发现 manifest 中的入口并校验
go-omni api inspect # 方法、路径、Handler、鉴权与业务错误总览
go-omni api generate --dry-run # 预览新增/更新/移除,完全不写文件
go-omni api generate # 实际生成,保留手写业务
go-omni api generate --check # 只读检查,可用于 CI
go-omni api export > openapi.json # 独立导出纯 JSON,无需启动服务
go test ./...
--dry-run 和 --check 不能组合。新项目预览也不创建目录;预览不是审批或锁定,实际生成会重新检查文件状态。
在项目外可显式传 --api;生成时传 --dir 可定位其他项目。不会猜测多个 .api 中哪个是入口。
路由总览的 (undeclared) 仅表示没有声明 auth;旧式中间件仍可能做认证。
外发文档可用 export;项目内受生成器管理的 docs/openapi.json 请通过 generate 更新,以同步文件哈希。
迁移扩展(v0.9.0-preview)
- 动态 JSON 字段
any/interface{},包含切片和 map;外围契约仍严格。 scalar Name (...)显式引用项目类型,声明 JSON 输入/输出类型及文本绑定;支持已有 JSON/Text 转换方法。@body (mode: json/raw/multipart; maxBytes: ...):按路由限制请求体。@binding custom生成保留的绑定 hook;@handlerMode manual生成保留的整个 Handler,raw/multipart 使用这个入口。- manual 可用
@produces "text/plain"等声明无 DTO 成功输出。默认状态 200,初始手写 stub 返回 501。
完整语法、边界和示例见 迁移扩展指南。新增生成代码要求 CLI 与框架依赖一起升级到 v0.9.0-preview;不兼容全部 goctl 语法。
参数语义
无 optional/default 的字段按“是否出现”判断必填,false、0、"" 是已提供的值。额外声明 binding:"required" 才会拒绝标量零值。默认值只在字段缺失时使用,不替换显式的空值。
业务需要区分“没传”和“传了零值”时使用 *T 加 optional。指针允许 JSON null;普通非指针字段拒绝 null(动态 any 允许);binding:"required" 拒绝指针 null。缺失与 null 都会得到 nil,本版没有三态 Nullable 类型。响应端的 optional 不等于 Go 编码器的 omitempty;需要省略零值时显式使用 omitempty。
JSON 必须是一个 object,Content-Type 为 application/json。默认最多 1 MiB,包含 chunked/未知长度和尾部空白;超限返回 413。重复 JSON key、未知字段、多个 JSON 值、标量 query/header 重复值、格式和约束错误返回 400;错误 Content-Type 返回 415。失败不会部分修改目标 DTO;错误详情只保留在内部 cause,公共响应不回显原始参数。
contract.Binder 在服务启动时编译,随后并发复用,不会每次请求重新分析标签。CLI 用相同编译器产生文档,因此来源、必填、默认、nullable、枚举、范围和长度具有同一来源。响应类型和约束由业务实现遵守,不在每次响应上再次校验;非 nullable 集合请返回空集合而不是 nil。
生成目录和业务边界
api/ # 开发者维护的契约及导入
cmd/server/main.go # 启动、日志、优雅退出
etc/config.yaml
internal/settings/
internal/servicecontext/ # 依赖初始化和释放
internal/types/typesGen.go
internal/apierrors/errorsGen.go # 声明错误后生成
internal/handler/routesGen.go
internal/handler/openapiGen.go
internal/handler/<group>/handlersGen.go
internal/service/<group>/service.go
internal/service/<group>/<operation>.go
internal/repository/<group>/repository.go
internal/middleware/registryGen.go
internal/middleware/<name>.go
docs/openapi.json
.goOmni/apiManifest.json
Service 方法接收标准 context.Context 与生成的请求 DTO;Handler 调用 Service 并完成响应包装。网络适配器仍经 Omni/httpx 接入。Repository 是业务所有的扩展位置,不自动生成虚构 CRUD 查询。
命名中间件按声明顺序组合,第一个在最外层。新 hook 初始返回 501,必须实现后才放行;registry 自动维护,hook 文件只创建一次。缺失 hook 在注册任何路由前报错。文档通过 x-omni-middleware 标出命名 hook,不把名称误当成已实现的认证协议。
安全更新与 CI
在生成项目中安装/使用与框架版本相符的 go-omni CLI:
go-omni api validate --api api/user.api
go-omni api generate --api api/user.api --dir .
go-omni api generate --api api/user.api --dir . --check
go test ./...
Gen.go 和 OpenAPI 由生成器管理,manifest 保存其 SHA-256。手动修改后更新会报冲突。Service、Repository、main、配置、依赖装配和中间件实现按字节保留;只补缺失文件。删除接口/分组时只移除旧的生成文件,保留业务代码;已删除类型仍被旧业务引用时,需要明确迁移该业务文件,编译器会提示。
--check 只读,检查产物是否过期,以及所需骨架是否缺失。契约复制到项目后,请编辑项目内的 API;外部定义与已存在副本不一致时会拒绝覆盖。一个项目由一个 API 入口管理,可以通过导入拆分文件。
生成器拒绝输出路径穿越、受管理路径上的符号链接、大小写路径冲突和无所有权的覆盖。生成前统一校验/格式化,锁文件防止两个生成器同时修改;文件以原子替换写入,普通写入失败会回滚。本版不是整个目录的崩溃原子事务:进程被强杀或机器断电后,应按 Git 恢复生成产物和 manifest,再重试。请将 manifest 与生成产物一并提交,也不要在生成过程中编辑这些文件。
生成器不自动安装依赖、不访问数据库、不推送仓库、不替业务实现权限规则。
验证
go test -race -p=2 ./contract ./openapi ./cmd/go-omni/...
python3 scripts/verifyAPI.py
Go 测试覆盖契约拒绝规则、多源绑定、零值/缺失、默认/范围/嵌套集合、文档约束、幂等更新、只读检查、业务保留、删除路由、路径边界及生成项目编译。Python 验收会在临时目录生成项目,分别启动 nethttp/native,验证真实 HTTP 请求、中间件、日志 requestId 和优雅退出;结束后清理自己创建的进程和临时文件。结果记录在 performance/apiGenerationLocal.json。