Files
go-omni/MIGRATION.md

10 KiB
Raw Permalink Blame History

已有 HTTP 项目逐步接入

从 v0.9.0-preview 开始,以下能力贯通运行时、.api、代码生成和 OpenAPI。CLI 和项目框架依赖一起升级;入口见 README。这些都是通用扩展点,框架不内置任何项目的 ID、支付回调、鉴权或数据库规则。

1. 保留标准 Handler 与中间件

app := omni.New(omni.WithLogger(true))
app.Use(httpx.WrapMiddleware(existingMiddleware)) // func(http.Handler) http.Handler
// go-zero 风格的 func(http.HandlerFunc) http.HandlerFunc:
app.Use(httpx.WrapFuncMiddleware(existingFuncMiddleware))
app.POST("/legacy", httpx.WrapHandler(http.HandlerFunc(existingHandler)))

实际使用时先配置所有 Use,再注册路由。WrapHandler 也接受 http.FileServer 等标准 http.Handler。

  • Handler 经过 App 的日志、requestId、恢复和应用中间件链,原来的 URL、状态码、Header、响应内容由它自己控制。
  • c.Raw() 的写入也会更新 c.Status() / c.Responded();404 会记为 404。提交响应后返回错误或 panic,不会再追加错误 JSON。103 等临时响应不算最终提交。
  • 桥接器不缓存完整响应。下载继续流式复制;底层支持时保留 Flusher、Hijacker、Pusher、ReaderFrom、CloseNotifier。http.ResponseController 可沿 Unwrap 访问底层控制能力。能力仍取决于网络适配器和其他中间件。
  • 请求 context、取消信号和已有值继续传递,Omni 的路径参数可用 Go 1.22 的 r.PathValue("id") 读取。其他框架私有的参数读取器仍需由项目适配。标准中间件应从原 r.Context() 派生上下文;不能换成无关的 Background,也不能在 ServeHTTP 返回后调用 next。异步 timeout middleware 不在此桥接器的契约内。
  • 标准中间件在每条注册链构造一次,状态会跨请求保留。下游返回的错误在标准中间件退出前编码,因此压缩、审计等中间件能处理错误响应。它们自己的缓冲或改写行为仍由项目负责。

调用流使用 App.Handle/GET/POST 等入口即可,不要绕到原始 Router 注册。暂不使用 .api 的旧接口也能这样接入。

2. 动态 JSON:允许渐进补全 DTO

type LegacyReply {
    Data any `json:"data"`
}

interface{} 是 any 的等价写法,支持 []any、map[string]any 和对应的 interface{} 写法。带方法的 interface 不支持,会提示明确的类型错误。

动态字段允许 object、array、string、number、bool、null;OpenAPI 字段 schema 为 {},不假装知道内部结构。它放开的是该字段的内容,外围 DTO 未声明的字段仍被拒绝,JSON 重复键、深度、总大小限制仍生效。动态数字解析为 json.Number,避免大整数先转 float64 丢精度。原始 JSON 的空白、转义和字节布局不会保留;验签请走原始 body 入口。

这不会自动补齐缺少的业务路由,也不代表兼容全部 goctl 语法。能确定结构的新接口优先使用命名 DTO。

3. 按路由限制 body 与保留手写入口

普通 JSON 路由只声明需要的大小,不改变其他路由默认的 1 MiB:

@body (mode: json
    maxBytes: 2097152
)
@handler createItem
post /items (CreateItemReq) returns (ItemResp)

maxBytes 单位为字节,允许 1 至 1 TiB;不写时为 1 MiB。已知超长或读取未知长度/chunked body 超限都按 413 处理。内置 App/RouterGroup 在全局中间件读取前安装 reader 限额;同时保持接口响应策略。网络层若有更小限额,以更小值为准。手写 Handler 吞掉读取错误或已经提交响应后,框架不能重新改写 HTTP 状态,务必检查读取错误。

只替换绑定函数

@binding custom
@body (mode: json
    maxBytes: 65536
)
@handler createItem
post /items (CreateItemReq) returns (ItemResp)

首次生成 internal/handler/<group>/createItemBind.go:

func (h *Handler) bindCreateItem(c *httpx.Context, req *types.CreateItemReq) error {
    // 调用项目自己的解码器,填充 req;失败返回 error。
}

Handler 继续调用 Service 并使用统一响应;只把绑定过程交给这个函数。初始 hook 返回 501,不能忽略实现。自定义绑定需自己实现 Content-Type、未知字段、尾部数据等校验,并遵守 DTO 文档;框架负责路由总大小限制,不隐式套第二次 JSON 绑定。

上传、原文验签、下载

@handlerMode manual
@body (mode: multipart
    maxBytes: 10485760
)
@handler upload
post /files

@handlerMode manual
@body (mode: raw
    maxBytes: 65536
)
@produces "text/plain"
@handler callback
post /callback

@handlerMode manual
@produces "application/octet-stream"
@handler download
get /download

首次生成 <operation>Manual.go,由项目实现整个 func(h *Handler) Callback(c *httpx.Context) error;依赖通过 h.deps 访问。生成器保留路由、中间件与文档管理,不会预读、JSON 解码或重新包装手写输出,也不生成这个路由的 Service 方法。需要调用 Service 时自行组合。

  • manual 无返回类型默认成功状态 200;也可 returns (UploadResp) 描述 JSON DTO,手写实现按声明调用 c.OK 等。@produces 用于无 DTO 的手写输出,描述成功响应媒体类型,不会自动设置运行时 Header。
  • raw 的文档为二进制请求体;multipart 为开放对象,具体文件字段与逐部分校验由项目实现。raw/multipart 请求 DTO 仅可含 path/form/header 字段,不能同时声明 JSON body 字段。form 始终指 URL query。
  • 原文验签直接读取 c.Request().Body,在声明的小限额内 io.ReadAll;大文件用 MultipartReader 配合 io.Copy 流式处理,不能整个读入内存。
  • 使用 ParseMultipartForm 时,其内存参数只是内存阈值,不是总上传限额。App/桥接器在请求结束(含错误和 panic)后删除临时文件;业务仍须关闭 FormFile 得到的文件。MultipartReader 不自动建临时文件,业务自行创建的文件由业务清理。
  • 手写 Handler 的成功输出由它控制;返回的 error、鉴权拒绝仍走接口的错误策略。三方若要求错误也返回指定文本,使用 自定义 Encoder 并维护对应错误文档;@produces 不会改变错误协议。

直接注册 Go 路由:

app.HandleWithBodyLimit("POST", "/upload", 10<<20, nil, uploadHandler)
// nil encoder 沿用 App 策略,也可传项目自己的 response.Encoder。

通用注册器使用 httpx.HandleWithBodyLimit(...);实现 httpx.BodyLimitRegistrar 可像 App 一样在全局中间件前限流量,未实现时只能在路由中间件入口限制。单独 httpx.LimitBody 同样受中间件顺序影响。

重新生成规则

handlersGen.go、routesGen.go、DTO、OpenAPI 继续由生成器管理;*Bind.go、*Manual.go 只首次创建,后续不会覆盖或删除。切换 manual 回自动模式时,先迁移实现并自行移除对应 Manual 文件,否则生成器明确拒绝,避免重复方法。修改 DTO 导致手写函数签名不匹配时,需要一起调整项目代码;go test ./... 是必要检查。

4. 自定义标量:项目定义行为,框架连接调用

例如输入 ID 兼容数字/字符串、输出统一为字符串:

scalar BusinessID (
    goImport: "example.com/billing/internal/ids"
    goType: ID
    jsonInput: string | int64
    jsonOutput: string
    text: true
)
type ItemReq {
    ID BusinessID `path:"id"`
}
type ItemResp {
    ID BusinessID `json:"id"`
}

goImport/goType 指向项目已有的具体 Go 类型,不是框架内置 ID。生成器创建类型别名和 types.ContractScalars(),然后把注册表显式传给每个 Binder。运行时使用标准接口:

项目类型的方法 用途
值接收者 MarshalJSON() ([]byte, error) JSON 输出
指针接收者 UnmarshalJSON([]byte) error JSON 输入
指针接收者 UnmarshalText([]byte) error text: true 时的 path/query/header

完整参考 示例 ID 类型。CLI 校验声明并生成代码,不执行项目 Go 代码;启动时校验方法是否齐全,缺失会明确失败。

输入可选 string、bool、int32、int64、float64,用 | 声明多个允许的 JSON 类型;输出只声明一个。框架先校验 JSON 类型和数字范围,再调用项目转换方法。字符串是否是合法 ID、是否允许空串或负数由项目方法决定,不能把数字溢出静默截断。path/query/header 实际传输文本,OpenAPI 对这些位置描述 string;query 切片通过重复键传递。

普通字段不受注册影响,普通 int64 仍拒绝 JSON 字符串。没有全局注册表,也不会自动给所有 ID 放宽规则。标量可嵌套、放入切片/map,*BusinessID 允许 null;缺失默认必填,optional/omitempty 可选。标量的额外约束放在转换器中,目前不叠加 binding、default/options/range 等标签。

请求 OpenAPI 按 string | int64 生成 anyOf,响应按 string 生成,避免把输入兼容误描述为输出也有两种类型。输出方法必须遵守声明,框架不会每次重新验证响应;项目需为自定义类型测试边界行为。

不使用 .api 也可显式注册:

binder := contract.MustNew[CreateReq](
    contract.ScalarOf[ids.ID]("string|int64", "string", true),
)
// binder.Bind(c.Request(), httpx.RouteParams(c.Context()), &req)

可运行示例与验收

go run ./examples/compatibility
curl -i http://127.0.0.1:8080/legacy
curl -i -X POST http://127.0.0.1:8080/echo \
  -H 'Content-Type: application/json' -d '{"id":9223372036854775807,"data":{"hello":true}}'
curl -i -F file=@README.md http://127.0.0.1:8080/upload
curl -OJ http://127.0.0.1:8080/download

完整契约:compatibility.api。生成项目起初的手写 hook 仍为 501;真实实现与原文 HMAC 验签回归见 消费者 fixture。

GO_OMNI_SCAFFOLD_INTEGRATION=1 go test -race ./cmd/go-omni/apigen -run Compatibility
go test -race ./httpx ./contract

这些是框架验收,不代表任何业务项目已经迁移完毕;业务的路由覆盖、响应兼容、权限、资金事务和数据库行为需要业务自己的回归。