# GoOmni GoOmni 是面向多个项目复用的 Go 基础框架。通用工程使用 `project new` 生成配置、日志、依赖装配、生命周期和普通 Go 运行入口;需要 HTTP 接口时可选择 `.api` 工程生成,也可以在已有项目中按需引入独立模块。运行方式、业务类型和外部依赖由项目决定。 **v0.28.2 改动**:API 响应 DTO 支持 `Children []Node`、`Next *Node`、递归 map 和相互引用,生成强类型 Go 字段及有限的 OpenAPI `$ref` 图。字段约束、示例校验、生成与漂移检查继续有效;请求递归和非法值递归仍会报错。见 [可运行示例](examples/recursiveResponse/README.md)与[接入说明](DEVELOPMENT.md#递归响应-dto)。 **v0.28.1 改动**:`workspace generate/check` 支持 API 与通用工程混合处理,可选 `kind: api | project`,省略时按生成清单识别。`format` 对通用工程显示跳过;检查只读,生成前统一预检。见 [混合工作区示例](examples/mixedWorkspace/README.md)。 **v0.28 改动**:新增独立的 `project new/generate/check`,默认无 API、监听端口、业务任务或外部服务配置;保护手写代码,并等待主运行入口和登记的后台工作结束后释放依赖。见 [通用工程指南](PROGRAM.md)和[可运行示例](examples/program/README.md)。 **v0.27 改动**:统一三种内置服务实现的默认/路由/硬请求体限额,配置冲突在启动前报错;停机以处理函数实际退出为资源清理前提,超时仍有请求时保留共享依赖。见 [HTTP 策略](transport/README.md#统一请求体限额)、[停机规则](lifecycle/README.md#http-请求与依赖的关闭屏障)和[可运行示例](examples/httpSafety/README.md)。 **v0.26 改动**:通用 [i18n](i18n/README.md),提供独立词典、语言匹配、回退、命名参数/条件/复数、可选 HTTP 与错误适配、检查/导出 CLI 及前端运行时。文案由项目维护;[完整示例](examples/i18n/README.md)可直接运行。CLI 与框架使用同版 v0.28.2-preview。同时修复 Go 1.22 下请求体限额错误的识别。 **从这份 README 开始接入。** 当前版本 **`v0.28.2-preview`**,通用工程采用根目录入口和 `config / svc / run`;API 工程使用 `config / svc / logic / handler`。可以固定版本开展业务接入;文件维护边界和已有服务迁移步骤见 [项目指南](PROJECT.md),版本变化与验收见 [发布说明](RELEASE.md)。 **v0.25 改动**:`database.InTx` 返回可识别的事务阶段与结果,保留业务/驱动及回滚错误链;提交报错保守标记为结果不确定,不等同于已回滚。[使用规则](database/README.md#事务阶段与结果)、[可运行故障示例](examples/transactions/README.md)。 **v0.24 改动**:新增可选 CORS,生成项目通过 `server.cors` 自动接入,支持预检、错误响应和明确的来源白名单;默认关闭。[配置与规则](cors/README.md)、[可运行示例](examples/cors/README.md)。 **v0.23 改动**:配置校验定位具体字段和安全原因;`httpc.JSON` 可选限制成功状态;`copyx.Check` 无需样本预检类型;`logx` 支持实例独立时区。[四项完整示例](examples/developerTools/README.md)。 **v0.19 改动**:新增可选 `copyx` 工具,用普通 Go 调用完成浅拷贝、深拷贝和显式类型转换;深拷贝会拒绝自定义转换中残留的源对象可变引用。**移除了 `mappings` / `mappingFiles` 和映射代码生成**,已有使用者须按 [迁移步骤](COPY.md#从旧-mapping-迁移) 调整,不能直接删除仍在调用的旧文件。 普通接口的流程是:**修改 `.api` → 生成 Request/Response → 写 Logic → 测试**。有独立职责的业务类型才需要转换;可直接写 Go,也可以按需使用 [copyx](COPY.md)。公共字段组合、中间件工厂、Repository 开关继续支持,见 [生成能力指南](GENERATION.md)。 Go **1.22+**。框架不依赖 Gin,默认使用标准库网络层,提供可切换的 native HTTP/1.1 和实验性 HTTP/2 适配器。 响应字段、业务码、登录方式、权限规则和数据库表结构由接入项目决定。文中的 `code/msg/data`、`200/9999` 都是示例配置,可以替换。 契约支持 `returns([]string/[]int64/[]bool/[]any)`、OpenAPI 公共引用、workspace 一次导出共享 Go 错误目录,以及文件显式环境引用与配置检查。详细配置与升级步骤见 [业务开发指南](DEVELOPMENT.md)。 已有通用开发能力见 [业务开发指南](DEVELOPMENT.md):数组/null、严格请求策略、声明式上传、文件显式配置引用、资源生命周期、共享契约和升级工具。MySQL 直接使用 [官方适配器](database/mysql/README.md)。 后台组件可在 svc 登记 [分阶段关闭](lifecycle/README.md#后台组件分阶段关闭),先停止接收工作并等待收尾,再关闭共享资源;无需绑定队列或 ORM。 使用 GORM 的项目可以选择 [gormx 适配器](database/gormx/README.md),复用数据库初始化和连接池,并将 SQL 日志接入 logx。 ## 当前功能与使用入口 下表是当前公开能力的索引。每项给出调用入口和完整说明;模块按需使用,`omni.New()` 不会自动初始化数据库、Redis、ORM、队列或云存储。示例中的 import 统一以 `git.yuntuo.hk/k2safe/go-omni/` 开头。 | 功能与用途 | 怎么用 | 解释与示例 | | --- | --- | --- | | 项目、接口与文档生成 | `go-omni new`;改 `.api` 后 `api generate`;`--check` 检查漂移 | [下方快速开始](#新项目生成并运行)、[CLI](cmd/go-omni/README.md)、[完整生成项目](examples/managedService/README.md) | | 公共字段与跨服务共享契约 | `.api` 组合对象字段;`omni.imports.json` 声明共享根;workspace 批量生成 | [公共字段示例](GENERATION.md#公共字段组合)、[共享契约与 workspace](DEVELOPMENT.md) | | 混合工程工作区 | `workspace generate/check` 按清单分发 API/project;format 仅处理 API | [可运行示例](examples/mixedWorkspace/README.md)、[工作区说明](DEVELOPMENT.md#混合-api-与通用工程) | | 通用 Go 工程 | `go-omni project new/generate/check`;配置、日志、依赖、运行与退出 | [通用工程指南](PROGRAM.md)、[完整可运行示例](examples/program/README.md) | | 中间件依赖工厂、可选 Repository | `omni.gen.json` 选择 factories / none;在 `New(deps)` 写一次实现 | [配置与完整工厂示例](GENERATION.md) | | 路由、分组、请求上下文 | `omni.New()`、`app.GET` / `Group` / `Use`;业务使用 `context.Context` | [基本服务](examples/basic/main.go)、[已有项目接入](#已有项目按模块接入) | | HTTP 网络与扩展适配 | 配置 transport 为 nethttp / native / http2;自定义 Router/Runner | [网络层配置与契约](transport/README.md)、[性能与限制](PERFORMANCE.md) | | 浏览器跨域 CORS | `server.cors` / `omni.WithCORS`;独立 HTTP 用 `cors.New(...).Wrap(handler)` | [配置、默认值与协议边界](cors/README.md)、[鉴权与预检示例](examples/cors/README.md) | | HTTP 老接口桥接 | 通过官方 Handler/Middleware 适配器接入标准 `net/http`,跟踪实际响应状态 | [接入说明](MIGRATION.md)、[桥接/上传/自定义 ID 示例](examples/compatibility/main.go) | | 请求校验、动态 JSON、自定义标量 | `.api` 声明来源/校验/scalar;手写契约用 `contract.New[T]`;简单绑定可用 `validate.JSON/Query/URI` | [语法与例子](API.md)、[请求策略](DEVELOPMENT.md) | | 数组、null、原始请求、上传下载 | `.api` 声明数组/可空返回、body/multipart 与大小限制;必要时使用 Bind/Manual 扩展 | [契约示例](examples/contracts)、[含上传/回调/下载的项目](examples/managedService/README.md) | | 公共响应与特殊协议 | `.api` 定义 response 并 `@app(defaultResponse: ...)`;Go 用 `WithResponse` / Encoder | [响应配置](RESPONSE.md)、[响应模板契约](examples/contracts/responses.api) | | 错误目录与动态提示 | `WithErrors(catalog)` 注册一次;`ByCode(...).WithParams(...).WithCause(err)` | [错误目录](errors/README.md)、[模板参数示例](examples/contracts/errorParams.api) | | 通用国际化 | `i18n.New/Render`;可选 `WithLanguagePolicy/WithErrorLocalizer`;`go-omni i18n check/export/types/extract` | [资源格式与边界](i18n/README.md)、[CLI/HTTP/前端示例](examples/i18n/README.md)、[生成契约](examples/contracts/i18n.api) | | OpenAPI 与错误公共引用 | `.api` 自动生成;手写接口可使用 `openapi` 注册器;Go 错误目录显式导出 | [契约工具](DEVELOPMENT.md)、[注册示例](examples/platform/main.go) | | 结构化业务日志 | `logx.Info/Error`、`logx.FromContext(ctx)`、`logx.New`;支持独立时区、轮转、脱敏、慢请求和级别更新 | [下方日志示例](#日志)、[完整配置](logx/README.md) | | 文件配置与环境引用 | Default()→JSON/YAML/XML 字段引用→类型绑定→校验;无隐式覆盖,支持 `-check-config` | [统一配置指南](CONFIG.md)、[完整可运行示例](examples/configuration/README.md)、[动态级别](ADVANCED.md#配置与日志热更新) | | 生命周期与健康检查 | svc 登记 OnClose/OnStop/OnDrain,RequestShutdown 通知故障;健康路径/开关可配 | [生命周期用法](lifecycle/README.md)、[健康检查](health/README.md)、[生成项目扩展](PROJECT.md) | | 数据库、连接池与事务 | `database.Open` / `OpenConnector`;`InTx` 的 `TxError` 提供阶段、已知结果与原因链 | [通用数据库](database/README.md)、[事务失败示例](examples/transactions/README.md)、[MySQL TLS/Logger](database/mysql/README.md) | | 可选 GORM 与迁移 | gormx 复用已有连接池;`go-omni migrate` 预览,显式 apply | [GORM 示例](database/gormx/README.md)、[迁移用法](ADVANCED.md#迁移审计与分页) | | 认证与权限接口 | 注入 Authenticator / Policy;JWT 或 Session 只是可选实现 | [认证说明](auth/README.md)、[鉴权完整例子](examples/platform/README.md) | | 缓存 | `cache.NewMemory` 或 Redis 实现;业务面向 Store 的 Get/Set/Delete | [缓存配置/代码](MODULES.md)、[可运行模块服务](examples/modules/README.md) | | 出站 HTTP、熔断 | `httpc.New` 配置代理、连接池和阶段超时;JSON 支持单次成功状态限制/请求头/限额;`resilience.New` 隔离故障 | [HTTP 完整用法](httpc/README.md)、[可运行示例](examples/httpClient/README.md)、[熔断/观测](ADVANCED.md#熔断与观测) | | 请求追踪、访问日志、恢复、限流、幂等、超时 | `app.Use(...)` 按需组合 middleware;跨副本选择 Redis 实现 | [模块说明与限制](MODULES.md#请求追踪与基础指标)、[分布式实现](ADVANCED.md#分布式限流与幂等) | | 本地后台任务与调度 | `task.New`、Submit、Every;有界队列、排空关闭 | [task 示例与生命周期](MODULES.md#后台执行task) | | 持久队列 | 业务使用 Publisher / Consumer;Redis Streams 提供至少一次交付 | [队列初始化/消费示例](ADVANCED.md#可靠队列) | | 文件存储、直传、云端分片 | Store 的 Put/Get/Delete;Local、S3/R2、OSS 按配置装配;扩展接口提供分片续传 | [存储接口与完整例子](storage/README.md)、[本地存储服务](examples/platform/README.md) | | 日志关联、指标与链路 | `metrics.New().Middleware()`;telemetry 注入 OTel exporter/reader | [基础指标](MODULES.md#请求追踪与基础指标)、[OTel 用法](ADVANCED.md#熔断与观测)、[完整接入](examples/platform/main.go) | | 审计与分页 | `audit.New` 注入 Sink/白名单;`pagination.Parse` / `NewResult` | [解释与调用例子](ADVANCED.md#迁移审计与分页) | | 可选结构转换与深拷贝 | `copyx.Copy` / `Clone` / `New`;`Check[S,T]` 预检类型,Go converter 处理差异 | [下方示例](#可选结构转换与深拷贝)、[完整 copyx 指南](COPY.md)、[可运行代码](examples/copying) | | 自定义适配器验收 | `transporttest.Run`、`storagetest.Run` / `RunUploads` 验证扩展契约 | [网络契约测试入口](transport/transporttest/router.go)、[存储契约测试入口](storage/storagetest/contract.go) | ## 通用工程快速开始 ```sh go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.28.2-preview go-omni project new demo --module example.com/demo cd demo go mod tidy go run . -check-config go run . # Ctrl+C / SIGTERM 退出;项目自己的 Run 也可以完成后直接返回。 go-omni project check go test ./... ``` 项目只修改配置、svc 依赖初始化和 Run;默认没有 HTTP 端口、数据库、队列或自动重试。`main.go`、加载和装配代码由生成器维护;手改受管文件会报冲突,手写文件保持。完整维护边界、异常退出和超时行为见 [PROGRAM.md](PROGRAM.md)。 ## 浏览器跨域 CORS 同源部署保持默认关闭;需要跨域时,在生成项目的配置文件中加入: ```yaml server: cors: enabled: true allowedOrigins: [https://admin.example.com, https://merchant.example.com] allowedMethods: [GET, HEAD, POST, PUT, PATCH, DELETE] allowedHeaders: [Authorization, Content-Type] exposedHeaders: [Content-Disposition, X-Request-ID] allowCredentials: false maxAge: 10m ``` 不需要修改 main、Logic 或 `.api`。框架在鉴权前处理合法预检;实际请求仍鉴权,401/404/405/500 保持项目响应格式并附带允许来源的跨域头。没有 Origin 的服务端回调正常处理。配置只决定浏览器读取权限,不替代登录、验签或 CSRF 防护。 手写项目用 `omni.WithCORS(omni.CORSConfig{...})`,独立 HTTP 服务用 `cors.New(...).Wrap(handler)`;代理已经处理 CORS 时保持关闭。完整 [默认值与边界](cors/README.md)、[可运行示例](examples/cors/README.md)。 ## 带认证的出站 HTTP HTTP Client 在 svc 初始化一次,业务重复使用。普通内部请求可直接选择直连和连接参数,无需手写 Transport: ```go client, err := httpc.New(httpc.Config{ Proxy: httpc.ProxyConfig{Mode: httpc.ProxyDirect}, Timeout: 3*time.Second, DialTimeout: time.Second, TLSHandshakeTimeout: time.Second, ResponseHeaderTimeout: time.Second, MaxConnsPerHost: 32, MaxIdleConnsPerHost: 16, MaxIdleConns: 64, }) if err != nil { return err } // 在 svc 生命周期登记 client.Close;关闭前先排空使用它的工作。 err = client.JSON(ctx, "GET", endpoint, nil, &result, httpc.WithHeader("Authorization", "Bearer "+token), httpc.WithMaxResponseBytes(64<<10), ) ``` 请求头只作用于本次调用,不把用户 Token 留在共享 Client。默认代理/参数保持原行为;也可选择环境代理或固定代理。JSON 自动关闭并限制响应,Do 保留下载/流式读取扩展。完整配置、重试边界和已有自定义 Transport 的迁移见 [httpc](httpc/README.md)。 ```sh go run ./examples/httpClient ``` ## 统一配置加载 业务只维护 Config、Default()、Validate() 和配置文件。生成器维护 Load/main;文件缺失字段保留默认值,显式 0/false/空字符串正常覆盖,固定值不会被同名环境变量再次覆盖。 ```go BatchSize int `json:"batchSize"` // 加入 Config;在 Default() 中设置 BatchSize: 100。 ``` ```yaml server: address: "${HTTP_ADDR:-127.0.0.1:8080}" batchSize: "${BATCH_SIZE:-100}" password: "${DB_PASSWORD}" sessionToken: "${SESSION_TOKEN:-}" ``` `${NAME}` 未设置或为空就报错;`${NAME:-value}` 明确指定备用值。`$$` 转义 `$`;密码中的冒号、换行、引号和 `${...}` 原样插入,不二次解释。JSON/YAML/XML 共用解析后的字段处理和 Go 类型绑定,错误同时定位文件、字段、变量及可用行列,不打印原值。 ```sh go run ./examples/configuration -format xml DEMO_CLIENT_TIMEOUT=500ms go run ./examples/configuration # 在生成项目中,仅检查选中的文件,不初始化依赖: go run . -config etc/config.test.yaml -check-config ``` 完整 [语义与迁移](CONFIG.md)、[可运行示例](examples/configuration/README.md)。**v0.22 移除旧自动环境覆盖、前缀/别名及 env/envDefault 标签**,生成项目先迁移开发者配置再重新生成;文件中的响应模板 `${code}` 写成 `$${code}`,`.api` 保持原样。 ## 选择接入方式 | 你的项目 | 从哪里开始 | | --- | --- | | 新建服务,先定义接口再开发 | [新项目:生成并运行](#新项目生成并运行) | | 已有服务,逐步接入 HTTP 或基础模块 | [已有项目:按模块接入](#已有项目按模块接入) | | 已有标准 Handler、动态 JSON、上传或自定义 ID 类型 | [迁移扩展指南](MIGRATION.md) | | 想统一返回体、业务错误或三方回调 | [配置公共响应与独立协议](#配置公共响应与独立协议) | | 要接日志、数据库、鉴权和存储 | [业务开发与依赖装配](#业务开发与依赖装配) | | 要切换网络层、配置 TLS 和退出清理 | [配置与运行](#配置与运行) | | 已接入,需要升级和检查生成代码 | [日常开发与升级](#日常开发与升级) | ## 准备环境与安装 module 地址是 `git.yuntuo.hk/k2safe/go-omni`。先确保本机 Go 和 Git 可用,并具有仓库读取权限;使用自己的 Git 凭据管理器,不把账号或 Token 写进 module 地址。 安装固定版本的 CLI,后面的命令继续使用同一终端。私有 module 加入 GOPRIVATE,保留已有配置: ```sh export GOPRIVATE="$(go env GOPRIVATE),git.yuntuo.hk" go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.28.2-preview export PATH="$(go env GOPATH)/bin:$PATH" go-omni --help ``` 如果设置过 GOBIN,将该目录加入 PATH。远程 CLI 默认引用自身版本的框架,生成的 go.mod 不需要本地 replace。开发者与 CI 使用同版 CLI;框架源码开发方式见 [CLI 文档](cmd/go-omni/README.md#版本与迁移)。 ## 新项目:生成并运行 在准备存放业务项目的目录执行,`./hello` 必须是新目录或空目录;把 `example.com/hello` 换成自己的 Go module 名称。 ```sh go-omni api new --module example.com/hello ./hello cd hello go mod tidy go run . ``` 另开终端验证: ```sh curl -i http://127.0.0.1:8080/api/greet/Alice curl -i http://127.0.0.1:8080/health/ready curl -sS http://127.0.0.1:8080/openapi.json ``` 第一个接口返回 HTTP 200,业务数据为 `{"message":"Hello Alice"}`。初始模板未配置响应格式,使用默认公共体: ```json {"code":"OK","message":"success","data":{"message":"Hello Alice"},"requestId":"本次请求编号"} ``` 按 Ctrl+C 停止服务。进入项目目录即可 `go build .`;该示例不需要数据库或其他外部服务。 ### 生成的项目放什么 ```text hello/ ├── api/service.api # 维护接口定义,也可 import 其他 .api ├── main.go # 生成器维护:启动、路由注册、优雅退出 ├── etc/config.yaml # 项目配置 ├── omni.gen.json # 项目维护的生成选项与可选映射 ├── internal/ │ ├── config/ # 配置读取 │ ├── svc/ # 数据库、日志、外部客户端等依赖 │ ├── types/*Gen.go # 按 .api 文件生成 DTO │ ├── handler/ # 生成的绑定、响应策略、路由及文档 │ ├── logic/greet/ # 在这里实现业务逻辑 │ ├── repository/ # 按需创建,默认不生成空骨架 │ └── middleware/ # 声明鉴权等中间件后生成工厂 ├── docs/openapi.json # 生成的 OpenAPI 3.0.3 └── .goOmni/apiManifest.json # 生成文件所有权与校验信息,一起提交 Git ``` 修改 `api/service.api` 后重新生成,业务写在 `internal/logic`,数据访问写在 `internal/repository`,依赖在 `internal/svc` 装配。声明业务错误后还会生成 `internal/apierrors/errorsGen.go`。 `api new` 的问候接口可以直接运行;自定义接口未实现的 Logic 返回 **501**。新项目的具名中间件工厂未实现时直接阻止启动;v0.16 的原有 handler 模式仍返回 501。生成器不会根据 HTTP 字段猜数据库表或业务规则。 ### 从自己的接口定义生成 保存下面的完整示例为 `catalog.api`,再执行后面的生成命令。这里的响应格式、错误码和提示都由这个业务项目定义。 ```text syntax = "v1" @app (defaultResponse: CommonResponse) response CommonResponse ( body: {"code":"${code}", "msg":"${message}", "data":"${data}"} codeType: int32 successCode: 200 errorCode: 9999 successMessage: "操作成功" dataMode: always codes: {"ITEM_NOT_FOUND":404001} ) error ItemNotFound 404 "ITEM_NOT_FOUND" "记录不存在" type GetItemReq { ItemID string `path:"itemId" binding:"required"` } type ItemResp { ItemID string `json:"itemId"` Name string `json:"name"` } @server ( group: item prefix: /api/v1 response: CommonResponse ) service catalog-api { @doc "查询商品" @errors ItemNotFound @handler getItem get /items/:itemId (GetItemReq) returns (ItemResp) } ``` ```sh go-omni api validate --api catalog.api go-omni new --api catalog.api \ --module example.com/catalog ./catalog cd catalog go mod tidy ``` 将生成的 `internal/logic/item/getItem.go` 替换为下面的演示实现。实际业务中换成 Repository 查询,Logic 返回业务数据或错误,不再手工包装公共体: ```go package item import ( "context" "example.com/catalog/internal/apierrors" "example.com/catalog/internal/types" ) func (s *Logic) GetItem(ctx context.Context, req *types.GetItemReq) (*types.ItemResp, error) { if req.ItemID != "1001" { return nil, apierrors.ItemNotFound() } return &types.ItemResp{ItemID: req.ItemID, Name: "示例商品"}, nil } ``` 示例 import 对应上面的 `example.com/catalog`,使用其他 module 名称时同步替换。启动后,在另一个终端验证: ```sh # catalog 项目目录中启动;先停掉前面的 hello 服务,释放 8080 端口。 go run . # 另一个终端: curl -i http://127.0.0.1:8080/api/v1/items/1001 curl -i http://127.0.0.1:8080/api/v1/items/missing ``` 成功时得到 `{"code":200,"msg":"操作成功","data":{"itemId":"1001","name":"示例商品"}}`;不存在时为 HTTP 404、`{"code":404001,"msg":"记录不存在","data":null}`。 需要 MySQL、文件存储或文件日志时,**首次生成**可以使用: ```sh go-omni new --api catalog.api \ --module example.com/catalogFull --database mysql --storage local \ --log-output file --log-format json ./catalogFull ``` 该命令在 `catalog.api` 所在目录执行,目标仍需是新目录。数据库迁移、表结构和 Repository 由项目维护,API 模板不自动建表;MySQL 使用结构化配置,密码通过 `GO_OMNI_DB_PASSWORD` 提供。存储还可选 `s3`、`r2`、`oss`,按生成项目的配置说明提供桶、端点和凭据。完整生成项目示例见 [managedService](examples/managedService/README.md)。 `go-omni new` 与 `go-omni api new` 统一生成契约项目。需要手写项目时,可直接引用框架包;生成器不再提供另一套 CRUD 启动目录。 ## 已有项目:按模块接入 在业务项目的 **go.mod 所在目录**执行: ```sh go get git.yuntuo.hk/k2safe/go-omni@v0.28.2-preview ``` 已有项目可直接复用现有目录、Service、Repository 和数据库连接,先选择一个接口接入。也可以只用 `logx`、`storage` 等包,暂时保留原来的 HTTP 框架。希望入口和 HTTP 接入都交给生成器的项目,按 [已有服务接入步骤](PROJECT.md#已有服务接入步骤) 在新目录生成后迁入业务代码。 下面是可独立运行的 `main.go` 示例;迁入现有项目时,把路由调用接到自己的业务服务即可。 ```go package main import ( "log" "net/http" omni "git.yuntuo.hk/k2safe/go-omni" "git.yuntuo.hk/k2safe/go-omni/logx" "git.yuntuo.hk/k2safe/go-omni/response" ) func main() { if err := run(); err != nil { log.Fatal(err) } } func run() error { closeLog, err := logx.Setup(logx.Config{ Service: "catalog", Level: "info", Format: "json", Output: "stdout", }) if err != nil { return err } defer closeLog() app := omni.New( omni.WithLogger(true), omni.WithResponse(response.Config{ Body: map[string]any{ "code": "${code}", "msg": "${message}", "data": "${data}", }, CodeType: "int32", SuccessCode: 200, ErrorCode: 9999, SuccessMessage: "操作成功", DataMode: "always", Codes: map[string]any{"ITEM_NOT_FOUND": 404001}, }), ) app.GET("/api/v1/items/:itemId", func(c *omni.Context) error { itemID := c.Param("itemId") logx.InfoContext(c.Context(), "item.get", "itemId", itemID) if itemID != "1001" { return omni.NotFound("ITEM_NOT_FOUND", "记录不存在") } return c.OK(map[string]string{"itemId": itemID, "name": "示例商品"}) }) // 第三方协议可明确输出文本,不经过公共 JSON 包装。 app.GET("/ping", func(c *omni.Context) error { return c.Text(http.StatusOK, "pong") }) return app.Run("127.0.0.1:8080") } ``` 运行 `go run .`,访问 `/api/v1/items/1001`、`/api/v1/items/missing` 和 `/ping`,可分别验证成功、业务错误及纯文本输出。`App` 实现标准 `http.Handler`,也可挂进现有 `http.Server` 或父路由;这时监听、TLS、信号和资源关闭由现有入口负责,结束时调用 `app.Close()`。 现有 `http.HandlerFunc` 可直接通过 `app.POST("/legacy", httpx.WrapHandler(http.HandlerFunc(oldHandler)))` 接入;状态码与访问日志一致,下载和流式输出保持原协议。标准中间件、动态 JSON、上传/原文 body、手写绑定和自定义类型的完整用法见 [迁移扩展指南](MIGRATION.md)。 接入现有业务时,逐项确认: - **协议**:保留既有 URL、HTTP 状态、字段、分页和空值语义。已有业务错误类型需要在项目内转换为 Omni 错误,或使用 `WithResponseEncoder` 适配原错误;设置字段模板不会自动识别其他库的错误。 - **鉴权**:把现有登录态和权限校验接入中间件,继续使用原来的授权范围和业务规则。原来依赖 `gin.Context` 等类型的 HTTP 代码需要适配;标准 `context.Context` 的业务服务可以继续复用。 - **数据类型**:ID、金额、日期等按项目协议转换。用 `.api` 的 `scalar` 显式关联项目自己的类型与 JSON/Text 转换方法,分别声明输入和输出协议;普通字段保持严格绑定,见 [自定义标量](MIGRATION.md#4-自定义标量项目定义行为框架连接调用)。 - **生成器**:它不会自动接管任意已有 Go 项目。希望引入 `.api` 时,先在独立新目录生成对照,再规划路由与生成目录的接入,不要用脚手架覆盖现有业务目录。 先通过一个接口的成功、参数错误、未登录、无权限及业务错误回归,再按模块迁移。框架不要求所有项目使用同一套业务目录或身份模型。 ## 配置公共响应与独立协议 | 接口来源或需求 | 配置位置 | | --- | --- | | `.api` 生成的接口 | `response Name (...)` 定义,`@server (response: Name)` 引用 | | 多个 `.api` 共用返回体 | 将声明放到 `common.api`,使用 `import "common.api"` | | 某个生成接口用另一种 JSON 公共体 | 路由声明 `@response OtherResponse` | | 只要业务 JSON、无成功包装 | 路由声明 `@response raw`;失败仍沿用组级格式 | | 手写路由和应用级 404/405 | `omni.WithResponse(response.Config{...})`,或读取配置后传入 `WithConfig` | | 成功和失败都要遵循特殊回调协议 | `HandleWithResponse` 绑定项目自己的 `response.Encoder` | | 单次直接输出 JSON、文本、XML、字节或空响应 | `c.JSON` / `c.Text` / `c.XML` / `c.Bytes` / `c.NoContent` | `${data}` 是业务返回值,`${message}` 是公开提示,`${code}` 是业务码;占位符保留原值类型,不是字符串拼接。还支持 `${requestId}`、`${ok}`、`${status}`,字段名和嵌套结构由项目决定。 - `codeType` 可以是 `string`、`int32`、`int64`;整数类型需要配置 `successCode` 和 `errorCode`。 - `codes` 把逻辑错误码映射成对外业务码;未单独映射的失败使用 `errorCode`。JSON 业务码不会改变 HTTP 状态,例如 `404001` 仍对应 HTTP 404。 - `successMessage` 设置成功提示;未设置 `errorMessage` 时沿用错误的公开提示。`messages` 可按逻辑错误码单独覆盖,优先级最高。全局 `errorMessage` 会替换未单独映射的所有失败提示,只有需要统一失败文案时才设置。 - `dataMode: always` 在无数据时输出 null;默认 `omitNil` 省略模板里相应对象字段。没有 `returns` 的 `.api` 路由默认 HTTP 204、无响应体,这与 `200 + data:null` 是不同的接口契约。 **生成接口的格式以 `.api` 为准**,修改后重新生成,运行时与 OpenAPI 一起更新;应用 YAML 不会覆盖它。404/405 没有匹配到某个 `.api` 接口,使用应用默认配置,需要统一时也配置应用默认值。 文本/XML 回调用 Go 编码器或直接输出,`.api` 可用 `@handlerMode manual` 与 `@produces` 声明成功媒体类型;特殊错误协议仍需项目维护。`@response raw` 仅表示原始 JSON。前面示例的 `c.Text` 控制这次正常返回;需要连鉴权失败、业务错误和 panic 都按回调协议响应时使用 `HandleWithResponse`。完整配置和回调示例见 [响应参考](RESPONSE.md)。 ## 业务开发与依赖装配 ### 日志 业务包直接使用 `logx`,不需要 HTTP 框架对象: ```go // ctx 是 Service 接收的标准 context.Context。 logx.InfoContext(ctx, "order.created", "orderId", orderID) logx.ErrorContext(ctx, "order.failed", "error", err) ``` 入口用 `logx.Setup` 初始化,退出时关闭;生成的 API 项目已经完成日志初始化和退出清理。Service、Repository 和外部请求持续传递请求的 `context.Context`,业务日志可关联 `requestId`,同时保留取消信号。框架不规定或自动注入 `userId`、`tenantId`。 `WithLogger(true)` 开启 HTTP 访问日志。格式、级别、文件轮转、脱敏键和慢请求阈值均可配置,见 [日志](logx/README.md)。 ### 鉴权与业务错误 `.api` 可声明认证方式和关联中间件,例如把下面的声明放入契约,并在相应 `@server` 块中加上 `auth: UserAuth`: ```text auth UserAuth ( type: bearer middleware: Auth ) ``` 新项目生成 `internal/middleware/authFactory.go`,由项目实现 `NewAuth(deps)`;生成器负责初始化和注册。v0.16 或 handler 模式继续使用 `internal/middleware/auth.go`。凭据校验和身份注入由项目实现。声明会生成路由绑定与 OpenAPI 的认证描述,**不会自动实现登录或授权规则**;未实现的工厂阻止启动,原有 hook 模式返回 501。手写路由可使用 `auth.RequireAuth(authenticator)` 或自己的中间件。JWT、Session 和 Policy 按需选择。 业务错误可以集中定义,业务只选择发生了哪个错误。**HTTP 状态由项目配置,业务失败可以返回 HTTP 200**;框架不根据 HTTP 状态单独判断成功与否。 错误目录支持两种维护方式,按项目选择一种:**Go 中定义并用 `omni.WithErrors` 注册一次**,或者在公共 `.api` 中声明并生成调用函数。前者不要求 `.api` 重复声明目录或逐路由列出错误;生成路由默认继承 App 目录。 采用 `.api` 管理时,其他契约可通过 `import` 引用: ```text errorCatalog CommonErrors ( defaultStatus: 200 errors: { "UsernameInvalid": {"key": "USERNAME_INVALID", "code": 400002, "message": "用户名格式不正确"}, "PasswordInvalid": {"key": "PASSWORD_INVALID", "code": 400002, "message": "密码格式不正确"}, "PermissionDenied": {"key": "PERMISSION_DENIED", "status": 403, "code": 403001, "message": "没有权限"} } ) ``` `UsernameInvalid` 是生成函数名;`key` 是唯一逻辑标识;`code` 是项目对外的字符串或整数业务码,可以重复。条目没有填写 `status` 时使用目录的 `defaultStatus`,未配置目录默认值时为 400。这个默认值只作用于该目录的条目,不会把所有框架异常都改成 200。 在已有公共响应中保留 `codeType: int32` / `int64` / `string`、成功码、未定义错误的兜底码和返回体模板;不再重复填写这几条错误的 `codes` / `messages`。通过 `@server (errors: UsernameInvalid, PasswordInvalid)` 或路由 `@errors UsernameInvalid` 引用它们。 生成后业务调用: ```go return TokenResponse{}, apierrors.UsernameInvalid() // 需要保留内部原因时: return TokenResponse{}, apierrors.UsernameInvalid().WithCause(err) ``` 生成器同时提供 `internal/apierrors.Catalog()`、错误函数、路由错误目录及 OpenAPI。目录会在 App 中间件执行前绑定到生成路由;无需手动改已经生成的 main。HTTP 200 的错误使用失败响应模板;OpenAPI 用 `anyOf` 同时保留成功 DTO 和错误体,多条错误共用数字码时仍保留各自示例。完整契约见 [errors.api](examples/contracts/errors.api)。 手写项目可在启动时加载自己的目录,保留原来的响应配置: ```go // omnierrors 是 git.yuntuo.hk/k2safe/go-omni/errors。 catalog := omnierrors.Catalog{ DefaultStatus: 200, Definitions: map[string]omnierrors.Definition{ "USERNAME_INVALID": {Code: 400002, Message: "用户名格式不正确"}, "PASSWORD_INVALID": {Code: 400002, Message: "密码格式不正确"}, }, } app := omni.New( omni.WithErrors(catalog), omni.WithResponse(response.Config{ CodeType: "int32", SuccessCode: 0, ErrorCode: 9999, MessageField: "msg", DataMode: "always", }), ) // Service 只返回标准 error,不依赖 App 或 HTTP Context: // return omnierrors.ByCode("USERNAME_INVALID").WithCause(err) ``` 目录也可放入 `config.Config.Errors`,由 JSON/YAML 加载;生成项目的路径为 `server.errors`,生成路由只有显式引用错误定义时才使用 `.api` 独立目录,未声明则继承 App。不同 App 独立持有配置,单路由也可通过 `response.WithErrors` 绑定专用目录与自定义 Encoder。直接输出文本、XML、下载或已提交的响应继续保留原协议。 访问日志保留实际 HTTP 状态,并增加失败标记、逻辑错误标识和目录业务码;指标、链路及框架标准中间件/幂等重放也保留失败语义。未知标识使用系统错误兜底,内部 cause 不写入默认公开响应。旧 `error (...)`、`New` / `BadRequest` / `Codes` / `Messages` 继续支持;新目录与旧映射冲突会报错,避免静默覆盖。 动态提示也可以集中配置,在目录中声明 `message: "{label}不能超过{max}个字符"` 和有序 `params`(label/string、max/int),生成带类型的调用: ```go return apierrors.TextTooLong("企业名称", 100) // 最终提示:企业名称不能超过100个字符 ``` 运行时支持 `omnierrors.ByCode("TEXT_TOO_LONG").WithParams(...)`,与生成函数共用解析器。固定错误继续无参数调用;缺参或类型错误使用系统兜底。模板、参数类型及具体响应示例同步写入 OpenAPI。完整示例见 [errorParams.api](examples/contracts/errorParams.api)。 更详细的配置、作用域和升级步骤见 [统一错误目录](errors/README.md)。 ### 可选结构转换与深拷贝 普通接口不需要任何拷贝配置,直接使用生成的 Request/Response。独立业务结构之间有转换需求时: ```go // import "git.yuntuo.hk/k2safe/go-omni/copyx" var input DomainInput if err := copyx.Copy(&input, req); err != nil { return err } // 源与目标之间的指针、map、slice 需要隔离时: snapshot, err := copyx.Clone(source) if err != nil { return err } ``` 默认浅拷贝,同名同类型字段自动赋值,保留零值;默认不会把 nil 填成非可空目标的零值。不要为使用工具新增一套重复模型。 需要重复使用或自定义规则时创建 Copier: ```go copier, err := copyx.New( copyx.WithDeepCopy(), copyx.WithFieldNames[Input, View](map[string]string{"Name": "DisplayName"}), copyx.WithConverter(func(v time.Time) (string, error) { return v.UTC().Format(time.RFC3339), nil }), ) if err != nil { return err } if err := copier.Copy(&view, input); err != nil { return err } ``` 自定义函数的源/目标类型由 Go 泛型确定。深拷贝时返回原指针、原 map、子切片或嵌套旧引用都会报错;字符串/整数原值可以返回。差异规则、nil 策略、字段覆盖、资源限制和原 mapping 移除步骤见 [copyx 指南](COPY.md),完整类型及可运行代码见 [examples/copying](examples/copying/main.go)。工具使用运行时检查,类型对需测试;深拷贝不适用于连接、锁等资源。 ### 减少接入代码:四个常用入口 ```yaml server: log: timeZone: Asia/Shanghai ``` 时区由 Logger 实例管理,不修改全局 `time.Local`。非法日志级别、数据库端口/连接池等配置会指出 `server.log.level`、`database.port` 等字段和原因,不打印输入值。详见 [配置诊断](CONFIG.md#框架模块的字段诊断)、[日志时区](logx/README.md#实例时区)。 ```go // 本次请求只接受 HTTP 200;默认仍接受所有 2xx。 err := client.JSON(ctx, http.MethodGet, url, nil, &result, httpc.WithSuccessStatuses(http.StatusOK), ) // CI/启动时检查类型,无需构造样本;仍使用 Copy 的同一组 Option。 err = copyx.Check[Request, BusinessInput](options...) ``` 状态不符合要求返回 `*httpc.StatusError`,不会写入 result,响应体仍由框架关闭。Check 不执行 converter;动态 interface、具体 nil/转换失败和深拷贝引用检查有明确边界,见 [类型预检](COPY.md#类型预检)。完整可运行代码见 [developerTools](examples/developerTools/README.md)。 ### 数据库、存储及其他模块 生成项目在 `internal/svc` 中创建依赖,提供给 Logic/Repository,并用 `c.OnClose(db.Close)` 登记自有资源。生成的 main 在 HTTP 排空后关闭 svc,再关闭日志。直接使用框架的手写项目则可以用 `app.OnShutdown(db.Close)`;不要对同一资源重复登记。 已有项目使用 `driver.Connector` 时,直接 `database.OpenConnector(ctx, connector, database.DefaultPoolConfig())`。MySQL 可直接用 [database/mysql](database/mysql/README.md) 配置独立 TLS、Logger、时区与拨号参数;任意驱动也可由项目构造 Connector。框架负责连接池、启动 Ping、健康检查及事务。无需转成 DSN,也无需注册全局 TLS/Logger。具体示例和资源归属见 [Connector 接入](database/README.md#connector-接入v091-preview)。 | 能力 | 包与接入说明 | | --- | --- | | 数据库连接池、事务、显式迁移 | `database`,见 [数据库](database/README.md);表结构与驱动由项目选择 | | 本地文件、S3、R2、OSS、直传与临时下载 | `storage`,见 [文件存储](storage/README.md);业务依赖接口,装配时选择适配器 | | 缓存、出站 HTTP、限流、幂等、后台任务 | `cache`、`httpc`、`middleware`、`task`,见 [基础模块](MODULES.md) | | JWT/Session、策略、Redis 队列、审计、分页、观测 | 见 [扩展模块](ADVANCED.md);按需注入实现 | | 存活与就绪检查 | `health`,见 [健康检查](health/README.md);数据库等依赖作为 readiness 探针 | API 项目的数据库迁移需要单独维护并显式执行;启动不会自动改表。完整示例 [userService](examples/userService/README.md) 展示 MySQL、事务、鉴权、日志和文件导出,可参考依赖装配方式后接入自己的业务规则。 ## 配置与运行 生成项目使用 **Default() → 选定文件的显式引用 → 类型绑定 → Validate()**。固定值不会被环境变量覆盖;配置规则、转义及迁移见 [配置指南](CONFIG.md)。手写入口显式加载配置,再传入 `omni.New(omni.WithConfig(settings))`。 | 文件字段 | 用途 | | --- | --- | | `server.address` | 监听地址,生成项目默认 127.0.0.1:8080 | | `server.transport` | nethttp(默认)、native、http2 | | `server.tls.certFile/keyFile` | 成对配置 TLS 证书和私钥路径 | | `server.http.readHeaderTimeout/readTimeout` | 请求头 / 请求读取超时 | | `server.http.writeTimeout/idleTimeout` | 响应写入 / Keep-Alive 空闲超时 | | `server.http.maxHeaderBytes` | 请求头大小限制,字节数 | | `server.http.defaultMaxBodyBytes` | 普通 App 路由默认请求体上限,零默认 8 MiB;路由可覆盖 | | `server.http.maxBodyBytes` | 服务请求体硬上限,路由不得超过;零不添加低于框架 1 TiB 的硬上限 | | `server.http.shutdownCancelTimeout` | 请求取消后等待处理函数收尾,零默认 5s;未结束则保留资源 | | `server.shutdownTimeout` | 优雅退出等待时间,默认 10s | | `server.enableLogger` / `server.log.level` | 访问日志开关、日志级别 | | `server.trustedProxies` | 可信代理 IP/CIDR,文件数组 | | `database.host/user/password/database` | 官方 MySQL 结构化连接配置 | 初始生成文件已为地址、网络层和选中模块的必要配置声明环境引用;其他字段按需要添加固定值或引用。 例如在生成项目中切换 native: ```sh GO_OMNI_TRANSPORT=native go run . ``` 手写入口可用 `omni.WithTransport(omni.TransportNative)`;TLS 配置为 `omni.WithTLS(certFile, keyFile)`。native 提供 HTTP/1.1、Keep-Alive、TLS、超时、连接/请求并发上限和对象复用;HTTP/2 是单独的实验适配器,必须配置 TLS。默认 nethttp 的 TLS 支持 HTTP/1.1 与 HTTP/2。 读写超时也可以在生成项目的 `etc/config.yaml` 中声明(放在 `server.http` 下): ```yaml server: http: readHeaderTimeout: 5s readTimeout: 30s writeTimeout: 30s idleTimeout: 60s maxHeaderBytes: 1048576 ``` 代码配置使用 `omni.WithHTTPConfig(omni.HTTPConfig{ReadTimeout: 30*time.Second, WriteTimeout: 30*time.Second})`。`Run`、`RunContext`、`ServeContext` 会应用它;`NewServer` 会配置返回的标准 `http.Server`,包含可选的 Go 字段 `ErrorLog`。**网络 I/O 参数正值覆盖、零值保留所选网络层的原有默认行为、负值报错**。请求体与取消收尾字段按上表解释;网络超时含义与支持范围见 [HTTP 配置](transport/README.md#http-配置v091-preview),不要把网络 I/O 超时当成业务执行时限。 外部宿主在注册完成后可用 `app.ValidateHTTP()` 预检配置与路由限额。上传路由使用 `app.HandleWithBodyLimit("POST", "/upload", 16<<20, nil, handler)` 或 `.api @request(maxBytes: 16777216)`;限额涵盖整个请求体。配置硬上限时至少允许该值;multipart 单文件、字段数以及幂等中间件缓存限制仍独立配置。新增普通路由默认 8 MiB;原 nethttp 手写大请求请显式设置默认值或路由上限,生成路由既有 `maxBytes`(默认 1 MiB)保持。 自研网络层通过 `RunWith(ctx, runner)` 接入监听与连接生命周期,通过 `NewWithRouter(router)` 替换路由;业务 Handler/Service 不需要导入具体适配器。细节与验收契约见 [网络适配层](transport/README.md)。 生成项目的依赖可调用 `svcCtx.RequestShutdown(err)` 主动申请退出,框架排空请求、清理依赖后保留故障原因并以非零状态退出。健康检查与 OpenAPI 的开关、路径通过 `server.endpoints` 配置,健康响应默认沿用项目公共体;未就绪保持 HTTP 503。完整配置和 v0.12 项目的升级步骤见 [项目指南](PROJECT.md#依赖主动申请退出)。 `Run` 处理 SIGINT/SIGTERM;`RunContext` 接收调用方控制的退出信号。先停止接收新请求并等待排空,超时取消请求、关闭连接,再等待 `http.shutdownCancelTimeout` 收尾窗口。只有处理函数和它的 defer 全部退出才清理资源;仍有请求时返回 `lifecycle.ErrResourcesRetained`,跳过 App 清理钩子。生成入口通过 `deps.Finish(runErr)` 同步保留数据库等依赖及日志,非零退出。`App.Close`、`Engine`、`NewServer`、`RunWith` 共用这个完成屏障;业务自行启动的 goroutine、Hijack 后的工作需另行注册后台生命周期。 ## 日常开发与升级 在已生成的 API 项目目录执行: ```sh go-omni api validate go-omni api inspect go-omni api generate --dry-run go-omni api generate go-omni api generate --check go test ./... ``` `main.go`、`Gen.go`、`docs/openapi.json` 和 manifest 由生成器管理,手动修改会报告冲突。Logic、Repository、中间件实现、配置和依赖装配属于项目,重复生成保留手写内容。契约变化会提示受影响的 Logic 文件和 DTO 字段,明确的签名错误会给出原签名与期望签名;生成后 `--check` 仍能检出未修复的问题。将 `.api`、`omni.gen.json`、生成文件和 `.goOmni/apiManifest.json` 一起提交,CI 同时运行 `generate --check` 与 `go test ./...`。详见 [契约变更提示与检查边界](PROJECT.md#契约变更提示)。 CLI 与业务 module 固定使用 `v0.28.2-preview`。v0.21 及更早项目先按 [配置迁移](CONFIG.md#v022-迁移旧模式已移除) 整理旧标签、文件与 Load,再升级依赖与 CLI 并重新生成,框架维护的 main/runtimeGen 自动更新;v0.13 及更早项目按 [升级预览](DEVELOPMENT.md#v013-项目升级) 处理,不直接覆盖手写入口或业务文件。使用过旧 mapping 的项目必须先完成 [移除迁移](COPY.md#从旧-mapping-迁移),旧配置或旧生成文件会阻止生成。升级后执行生成检查与业务回归。版本变化、迁移边界和验收记录见 [发布说明](RELEASE.md)。 ## 按需深入 | 需要了解 | 文档 | | --- | --- | | `.api` 类型、参数绑定、错误、鉴权和生成规则 | [API 开发指南](API.md) | | 公共响应体、提示优先级、独立回调协议 | [响应参考](RESPONSE.md) | | CLI 完整参数、本地框架开发与迁移 | [CLI](cmd/go-omni/README.md) | | 当前生成项目:Go 错误目录、上传、回调和下载 | [managedService](examples/managedService/README.md) | | 数据库/事务业务参考(旧目录示例) | [userService](examples/userService/README.md) | | 手写接口分层示例 | [reference](examples/reference/README.md) | | 框架成熟度、历史变更 | [路线图](ROADMAP.md)、[更新记录](CHANGELOG.md) | | 网络性能、测试条件与适用范围 | [性能说明](PERFORMANCE.md)、[HTTP/2 验收](transport/native/HTTP2.md) | 当前版本可用于固定版本接入与业务验证,仍标记为 preview。`.api` 借鉴 go-zero 的接口开发方式,支持范围以本仓库文档为准,不是完整 goctl 语法兼容层;HTTP/2 保持实验状态,OSS/R2 云账号实网验收不属于本次发布范围。 ## 后续开发约定 每次功能开发完成,同步更新这份功能索引、用法说明、`examples/` 和发布记录;删除或改变功能必须注明影响与迁移。代码、示例和文档一起回归后再发布。约定已记录在 [AGENTS.md](AGENTS.md),后续开发继续遵循。