Files
go-omni/README.md

25 KiB
Raw Permalink Blame History

GoOmni

GoOmni 是面向多个业务项目复用的 Go 后端框架:用 .api 定义接口、请求和返回类型,生成项目目录、HTTP 接入代码及 OpenAPI,业务代码负责 Service 和 Repository。也可以在已有项目中直接使用 omni.New(),或单独引入日志、存储等模块。

从这份 README 开始即可接入。 当前版本为 v0.10.0-preview,Go 1.22+;CLI 和项目依赖使用同一固定版本。框架不依赖 Gin,默认使用标准库网络层,提供可切换的 native HTTP/1.1 和实验性 HTTP/2 适配器。

响应字段、业务码、登录方式、权限规则和数据库表结构由接入项目决定。文中的 code/msg/data、200/9999 都是示例配置,可以替换。

选择接入方式

你的项目 从哪里开始
新建服务,先定义接口再开发 新项目:生成并运行
已有服务,逐步接入 HTTP 或基础模块 已有项目:按模块接入
已有标准 Handler、动态 JSON、上传或自定义 ID 类型 迁移扩展指南
想统一返回体、业务错误或三方回调 配置公共响应与独立协议
要接日志、数据库、鉴权和存储 业务开发与依赖装配
要切换网络层、配置 TLS 和退出清理 配置与运行
已接入,需要升级和检查生成代码 日常开发与升级

准备环境与安装

module 地址是 git.yuntuo.hk/k2safe/go-omni。先确保本机 Go 和 Git 可用,并具有仓库读取权限;使用自己的 Git 凭据管理器,不把账号或 Token 写进 module 地址。

以下命令在同一个终端执行。已有 GOPRIVATE 时,把 git.yuntuo.hk 追加到原来的逗号分隔列表。

export GOPRIVATE=git.yuntuo.hk
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.10.0-preview
export PATH="$(go env GOPATH)/bin:$PATH"
go-omni --help

如果配置了自定义 GOBIN,将那个目录加入 PATH。只引用框架包、不生成项目时可以跳过 CLI 安装,直接使用下文的 go get。

新项目:生成并运行

在准备存放业务项目的目录执行,./hello 必须是新目录或空目录;把 example.com/hello 换成自己的 Go module 名称。

go-omni api new --framework-version v0.10.0-preview \
  --module example.com/hello ./hello
cd hello
go mod tidy
go run ./cmd/server

另开终端验证:

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"}。初始模板未配置响应格式,使用默认公共体:

{"code":"OK","message":"success","data":{"message":"Hello Alice"},"requestId":"本次请求编号"}

按 Ctrl+C 停止服务。项目依赖已固定为远程版本,不需要克隆框架源码或添加本地 replace。

生成的项目放什么

hello/
├── api/service.api                  # 维护接口定义,也可 import 其他 .api
├── cmd/server/main.go               # 启动、路由注册、优雅退出
├── etc/config.yaml                  # 项目配置
├── internal/
│   ├── settings/                    # 配置读取
│   ├── servicecontext/              # 数据库、日志、外部客户端等依赖
│   ├── types/typesGen.go            # 生成的请求与业务返回类型
│   ├── handler/                     # 生成的绑定、响应策略、路由及文档
│   ├── service/greet/               # 在这里实现业务逻辑
│   ├── repository/greet/            # 在这里实现数据访问
│   └── middleware/                  # 声明鉴权等中间件后生成对应 hook
├── docs/openapi.json                # 生成的 OpenAPI 3.0.3
└── .goOmni/apiManifest.json         # 生成文件所有权与校验信息,一起提交 Git

修改 api/service.api 后重新生成,业务写在 internal/service,数据访问写在 internal/repository,依赖在 internal/servicecontext 装配。声明业务错误后还会生成 internal/apierrors/errorsGen.go。

api new 的问候接口可以直接运行;自定义接口新生成的 Service 和鉴权 hook 返回 501,需要实现后才可用。生成器不会根据 HTTP 字段猜数据库表或业务规则。

从自己的接口定义生成

保存下面的完整示例为 catalog.api,再执行后面的生成命令。这里的响应格式、错误码和提示都由这个业务项目定义。

syntax = "v1"

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)
}
go-omni api validate --api catalog.api
go-omni new --api catalog.api --framework-version v0.10.0-preview \
  --module example.com/catalog ./catalog
cd catalog
go mod tidy

将生成的 internal/service/item/getItem.go 替换为下面的演示实现。实际业务中换成 Repository 查询,Service 返回业务数据或错误,不再手工包装公共体:

package item

import (
    "context"

    "example.com/catalog/internal/apierrors"
    "example.com/catalog/internal/types"
)

func (s *Service) 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 名称时同步替换。启动后,在另一个终端验证:

# catalog 项目目录中启动;先停掉前面的 hello 服务,释放 8080 端口。
go run ./cmd/server
# 另一个终端:
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、文件存储或文件日志时,首次生成可以使用:

go-omni new --api catalog.api --framework-version v0.10.0-preview \
  --module example.com/catalogFull --database mysql --storage local \
  --log-output file --log-format json ./catalogFull

该命令在 catalog.api 所在目录执行,目标仍需是新目录。数据库迁移、表结构和 Repository 由项目维护,API 模板不自动建表;MySQL DSN 通过 GO_OMNI_DB_DSN 提供。存储还可选 s3、r2、oss,按生成项目的配置说明提供桶、端点和凭据。完整装配例子见 userService。

不采用 .api 时,也可以用 go-omni new --framework-version v0.10.0-preview --module example.com/billing ./billing 创建手写接口的 CRUD 示例项目;两种生成方式选择一种即可。

已有项目:按模块接入

在业务项目的 go.mod 所在目录执行:

go get git.yuntuo.hk/k2safe/go-omni@v0.10.0-preview

已有项目可直接复用现有目录、Service、Repository 和数据库连接,先选择一个接口接入。也可以只用 logx、storage 等包,暂时保留原来的 HTTP 框架。

下面是可独立运行的 main.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、手写绑定和自定义类型的完整用法见 迁移扩展指南。

接入现有业务时,逐项确认:

  • 协议:保留既有 URL、HTTP 状态、字段、分页和空值语义。已有业务错误类型需要在项目内转换为 Omni 错误,或使用 WithResponseEncoder 适配原错误;设置字段模板不会自动识别其他库的错误。
  • 鉴权:把现有登录态和权限校验接入中间件,继续使用原来的授权范围和业务规则。原来依赖 gin.Context 等类型的 HTTP 代码需要适配;标准 context.Context 的业务服务可以继续复用。
  • 数据类型:ID、金额、日期等按项目协议转换。用 .api 的 scalar 显式关联项目自己的类型与 JSON/Text 转换方法,分别声明输入和输出协议;普通字段保持严格绑定,见 自定义标量。
  • 生成器:它不会自动接管任意已有 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。完整配置和回调示例见 响应参考。

业务开发与依赖装配

日志

业务包直接使用 logx,不需要 HTTP 框架对象:

// 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 访问日志。格式、级别、文件轮转、脱敏键和慢请求阈值均可配置,见 日志。

鉴权与业务错误

.api 可声明认证方式和关联中间件,例如把下面的声明放入契约,并在相应 @server 块中加上 auth: UserAuth:

auth UserAuth (
    type: bearer
    middleware: Auth
)

生成器会创建 internal/middleware/auth.go,由项目实现凭据校验和身份注入。声明会生成路由绑定与 OpenAPI 的认证描述,不会自动实现登录或授权规则;未实现的 hook 返回 501。手写路由可使用 auth.RequireAuth(authenticator) 或自己的中间件。JWT、Session 和 Policy 按需选择。

业务错误可以集中定义,业务只选择发生了哪个错误。HTTP 状态由项目配置,业务失败可以返回 HTTP 200;框架不根据 HTTP 状态单独判断成功与否。

API 项目推荐在公共 .api 中声明目录,其他契约通过 import 引用:

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 引用它们。

生成后业务调用:

return TokenResponse{}, apierrors.UsernameInvalid()
// 需要保留内部原因时:
return TokenResponse{}, apierrors.UsernameInvalid().WithCause(err)

生成器同时提供 internal/apierrors.Catalog()、错误函数、路由错误目录及 OpenAPI。目录会在 App 中间件执行前绑定到生成路由;无需手动改已经生成的 main。HTTP 200 的错误使用失败响应模板;OpenAPI 用 anyOf 同时保留成功 DTO 和错误体,多条错误共用数字码时仍保留各自示例。完整契约见 errors.api。

手写项目可在启动时加载自己的目录,保留原来的响应配置:

// 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 独立持有配置,单路由也可通过 response.WithErrors 绑定专用目录与自定义 Encoder。直接输出文本、XML、下载或已提交的响应继续保留原协议。

访问日志保留实际 HTTP 状态,并增加失败标记、逻辑错误标识和目录业务码;指标、链路及框架标准中间件/幂等重放也保留失败语义。未知标识使用系统错误兜底,内部 cause 不写入默认公开响应。旧 error (...)、New / BadRequest / Codes / Messages 继续支持;新目录与旧映射冲突会报错,避免静默覆盖。

更详细的配置、作用域和升级步骤见 统一错误目录。

数据库、存储及其他模块

依赖在启动入口或项目自己的 internal/servicecontext 中创建,通过构造函数传给 Service/Repository,避免每个请求都新建连接池。只注册自己拥有的资源,例如 app.OnShutdown(db.Close);关闭按注册的逆序执行。

已有项目使用 driver.Connector 时,直接 database.OpenConnector(ctx, connector, database.DefaultPoolConfig())。MySQL 等驱动的独立 TLS、Logger、时区与拨号参数由项目构造 Connector;框架负责连接池、启动 Ping、健康检查及事务。无需转成 DSN,也无需注册全局 TLS/Logger。具体示例和资源归属见 Connector 接入。

能力 包与接入说明
数据库连接池、事务、显式迁移 database,见 数据库;表结构与驱动由项目选择
本地文件、S3、R2、OSS、直传与临时下载 storage,见 文件存储;业务依赖接口,装配时选择适配器
缓存、出站 HTTP、限流、幂等、后台任务 cache、httpc、middleware、task,见 基础模块
JWT/Session、策略、Redis 队列、审计、分页、观测 见 扩展模块;按需注入实现
存活与就绪检查 health,见 健康检查;数据库等依赖作为 readiness 探针

API 项目的数据库迁移需要单独维护并显式执行;启动不会自动改表。完整示例 userService 展示 MySQL、事务、鉴权、日志和文件导出,可参考依赖装配方式后接入自己的业务规则。

配置与运行

生成项目从 etc/config.yaml 读取配置,优先级为 默认值 → 配置文件 → 环境变量。密钥通过环境变量提供。手写入口需显式调用配置加载函数并传给 omni.New(omni.WithConfig(settings));单独调用 omni.New() 不会自动读取环境变量。

环境变量 用途
GO_OMNI_HTTP_ADDR 监听地址;生成项目默认 127.0.0.1:8080
GO_OMNI_TRANSPORT nethttp(默认)、native、http2
GO_OMNI_TLS_CERT_FILE / GO_OMNI_TLS_KEY_FILE 成对设置 TLS 证书和私钥路径
GO_OMNI_HTTP_READ_HEADER_TIMEOUT / GO_OMNI_HTTP_READ_TIMEOUT 请求头 / 请求读取超时,如 5s / 30s
GO_OMNI_HTTP_WRITE_TIMEOUT / GO_OMNI_HTTP_IDLE_TIMEOUT 响应写入 / Keep-Alive 空闲超时
GO_OMNI_HTTP_MAX_HEADER_BYTES 请求头大小限制,字节数
GO_OMNI_SHUTDOWN_TIMEOUT 优雅退出等待时间,默认 10s
GO_OMNI_ENABLE_LOGGER / GO_OMNI_LOG_LEVEL 访问日志开关、日志级别
GO_OMNI_TRUSTED_PROXIES 可解析转发地址头的可信代理 IP/CIDR,逗号分隔
GO_OMNI_DB_DSN 选择 MySQL 初始化时的数据库连接配置

例如在生成项目中切换 native:

GO_OMNI_TRANSPORT=native go run ./cmd/server

手写入口可用 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 下):

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。正值覆盖、零值保留所选网络层的原有默认行为、负值报错;不配置时升级行为不变。超时含义与支持范围见 HTTP 配置,不要把网络 I/O 超时当成业务执行时限。

自研网络层通过 RunWith(ctx, runner) 接入监听与连接生命周期,通过 NewWithRouter(router) 替换路由;业务 Handler/Service 不需要导入具体适配器。细节与验收契约见 网络适配层。

Run 处理 SIGINT/SIGTERM;RunContext 接收调用方控制的退出信号。服务先停止接收新请求,等待活跃请求完成,再清理资源;超时会取消请求并关闭连接,业务代码仍需响应 context 取消。

日常开发与升级

在已生成的 API 项目目录执行:

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 ./...

Gen.go、docs/openapi.json 和 manifest 由生成器管理,手动修改会报告冲突。Service、Repository、中间件实现、main、配置和依赖装配属于项目,重复生成保留手写内容;契约改变后仍需自行调整业务签名并通过编译。将 .api、生成文件和 .goOmni/apiManifest.json 一起提交,CI 可运行 generate --check 检查是否同步。

升级时先安装目标版本的 CLI,再在业务 module 中 go get 同一版本;API 项目随后执行上述生成与测试步骤,手写项目只需依赖升级及业务回归。v0.10.0-preview 的变化、迁移边界和验收记录见 发布说明。

按需深入

需要了解 文档
.api 类型、参数绑定、错误、鉴权和生成规则 API 开发指南
公共响应体、提示优先级、独立回调协议 响应参考
CLI 完整参数、本地框架开发和模块脚手架 CLI
完整 API 业务项目 userService
手写接口分层示例 reference
框架成熟度、历史变更 路线图、更新记录
网络性能、测试条件与适用范围 性能说明、HTTP/2 验收

当前版本可用于固定版本接入与业务验证,仍标记为 preview。.api 借鉴 go-zero 的接口开发方式,支持范围以本仓库文档为准,不是完整 goctl 语法兼容层;HTTP/2 保持实验状态,OSS/R2 云账号实网验收不属于本次发布范围。