Files
go-omni/errors/README.md

15 KiB
Raw Blame History

统一错误目录

v0.26 新增的通用多语言适配见 i18n。目录条目可显式引用 translation: {namespace, key};errori18n.Bind/Source 支持从同一份 Go 目录生成源词典。翻译只影响公开展示,不改变业务码、HTTP 状态、失败语义、参数契约或 cause。没有引用或没有启用适配时沿用现有行为;幂等重放保留首次语言。该能力需要 v0.26.0-preview 或更新版本。

从 v0.10.0-preview 起,业务可以只返回逻辑错误标识,框架从项目目录取得 HTTP 状态、对外业务码与公开提示。目录不依赖具体业务、数据库、身份模型或网络层,也不定义固定返回体。

基本规则

  • key / Definitions 的 map key 是唯一逻辑标识,例如 USERNAME_INVALID、user.notFound。API 新目录允许项目自己的标识命名;旧 error (...) 声明保持原有大写标识规则。
  • code 是对外业务码,支持字符串、整数,同一个码可以由多个不同标识使用。JSON/YAML 和 .api 读取整数时保留 int64 精度。
  • DefaultStatus / defaultStatus 只为目录中未写状态的条目提供默认值,省略时为 400。项目可设为 200;单项 Status / status 优先。
  • 目录条目允许有响应体的最终 HTTP 状态:200..599,排除 204、205、304。成功输出与错误输出分开;HTTP 200 并不把错误变成成功。
  • 数字码类型与范围遵守选中响应的 codeType(string/int32/int64;integer 是旧别名)。不同路由可以选择不同类型的公共响应与目录。
  • 新目录声明完整公开提示;每条错误的业务码和提示优先于统一 errorCode / errorMessage 兜底。旧的同标识 codes / messages 若与目录冲突,初始化或契约编译报错;相同值允许保留,便于逐步迁移。

手写项目

import (
    omni "git.yuntuo.hk/k2safe/go-omni"
    omnierrors "git.yuntuo.hk/k2safe/go-omni/errors"
    "git.yuntuo.hk/k2safe/go-omni/response"
)

catalog := omnierrors.Catalog{
    DefaultStatus: 200,
    Definitions: map[string]omnierrors.Definition{
        "USERNAME_INVALID": {Code: 400002, Message: "用户名格式不正确"},
        "PASSWORD_INVALID": {Code: 400002, Message: "密码格式不正确"},
        "FORBIDDEN": {Status: 403, Code: 403001, Message: "没有权限"},
    },
}
app := omni.New(
    omni.WithErrors(catalog),
    omni.WithResponse(response.Config{
        CodeType: "int32", SuccessCode: 0, ErrorCode: 9999,
        MessageField: "msg", DataMode: "always",
    }),
)

Service 返回 omnierrors.ByCode("USERNAME_INVALID"),或 .WithCause(err)。业务无需接收 App/HTTP Context。目录属于 App,初始化时校验并复制;后续修改原 map 不影响正在服务的应用。配置快照也不暴露内部 map。

NewRegistry(catalog) / MustRegistry(catalog) 可在 CLI/任务消费者等非 HTTP 入口使用;registry.Resolve(err) 返回独立的 *APIError,包含逻辑 Code、对外 BusinessCode、Status、Message 与内部 Cause。每次调用都创建新对象;不要自行将一个可变错误对象保存为共享单例再调用 WithCause。

原有 New(status, key, message) 等明确错误也能被同标识目录条目覆盖;未在目录中的明确错误沿用原状态与提示。未配置目录时,原有行为保持。Go 目录允许 Definition.Code == nil 以继续使用旧 Codes/errorCode 映射;新 .api errorCatalog 要求显式 code。

Go 维护错误目录,生成路由继承 App(v0.11.1-preview)

错误可以只在 Go 中定义,并在 omni.New(omni.WithErrors(catalog), ...) 注册一次。.api 只写接口、请求/响应 DTO 和响应格式,无需再声明 errorCatalog,也无需为每条路由列出错误。业务直接返回 ByCode(key) 或 ByCode(key).WithParams(...)。

选择规则:

  • 生成路由没有有效的 @errors / @server errors 列表时,继承 App 目录,包括 App 配置的 INTERNAL_ERROR 中文兜底。
  • 路由显式引用了错误定义时,使用该路由自己的目录;不与 App 目录合并,保持独立语义。
  • @errors none 清空 .api 分组继承的错误列表;清空后按没有路由目录处理,继承 App,而不是禁用错误处理。
  • 错误目录与响应格式独立选择。继承 App 错误不覆盖 .api 的响应模板、RawJSON、绑定方式和请求体限额;响应 codeType 需能表示目录中的业务码。
  • App 全局中间件提前返回错误、普通 Handler 和 Context 克隆遵循相同规则。

response.WithErrors / MustWithErrors 收到空目录时返回原 Encoder,不建立空 Registry,也不在编码时再次解析原始 error。已有 v0.11.0 生成代码中的空包装因此也能通过升级运行时修复;同步升级 CLI 重新生成则不再产生空包装。

可生成的示例见 errorInheritance.api。Go 目录是在运行时注册的,CLI 不执行项目 Go 代码;静态 OpenAPI 保留 .api 中的响应结构,不会自动枚举 Go 目录的具体错误码、提示及状态。这不影响运行时接入,也不要求重复维护目录。

文件配置

下面是生成项目的 etc/config.yaml 结构;直接 config.LoadFile 加载框架配置时去掉外面的 server。手写 main 需显式将加载结果传入 WithConfig。

server:
  errors:
    defaultStatus: 200
    definitions:
      USERNAME_INVALID:
        code: 400002
        message: 用户名格式不正确
      FORBIDDEN:
        status: 403
        code: 403001
        message: 没有权限
  response:
    codeType: int32
    successCode: 0
    errorCode: 9999
    messageField: msg
    dataMode: always

重复 JSON/YAML key、未知字段、非法状态、非整数数值及类型/范围错误会拒绝加载。目录不支持运行时热更新;修改后重新启动,避免请求与文档在运行期间漂移。

.api:定义一次,生成调用与文档

完整示例见 errors.api。语法:

errorCatalog CommonErrors (
    defaultStatus: 200
    errors: {
        "UsernameInvalid": {"key": "USERNAME_INVALID", "code": 400002, "message": "用户名格式不正确"},
        "PasswordInvalid": {"key": "PASSWORD_INVALID", "code": 400002, "message": "密码格式不正确"}
    }
)

目录可放进公共 .api 再 import;也可以拆成多个命名目录,各自设置默认状态。生成器合并定义时检查目录名、函数名、逻辑标识冲突。Catalog 是生成包的保留函数名。目录对象的键是生成的 UpperCamelCase 函数名,条目的 key 是运行时逻辑标识,二者用途不同。

在 @server 的 errors 或路由 @errors 中引用函数名,响应仍通过原有 response 声明选择:

@server (
    response: CommonResponse
    errors: UsernameInvalid, PasswordInvalid
)
service account-api {
    @handler token
    post /token returns (TokenResponse)
}

生成结果包括:

  • apierrors.UsernameInvalid() 等无参数函数,每次返回独立错误,支持 WithCause。
  • apierrors.CodeUsernameInvalid 等逻辑标识常量。
  • apierrors.Catalog():整个契约的可复制运行时目录,方便手写路由复用。
  • 自动绑定到生成路由的目录:只包含该路由通过 @errors / @server errors 声明的错误,在 App 全局中间件之前生效,支持认证提前返回和缓冲克隆。
  • OpenAPI 的错误响应与示例。同一 HTTP 200 下用 anyOf 同时描述成功 DTO 和错误体;多个错误共用数字码时保持多个示例;raw 成功体与失败公共体也分开描述。

业务直接 return TokenResponse{}, apierrors.UsernameInvalid()。也可以返回 omnierrors.ByCode(apierrors.CodeUsernameInvalid);使用标识引用时,当前生效的目录必须包含该错误。采用 .api 管理路由独立目录时,新增错误需补充该路由的 @errors;采用 Go 注册 App 目录时,无需在 .api 重复声明。

显式声明了错误列表的生成路由使用 .api 目录,未声明的路由继承 App;响应格式始终按各自配置选择。手写路由可在 main 用 omni.WithErrors(apierrors.Catalog()) 复用目录并选择兼容的 WithResponse。已有手写文件保持不动。

提示模板参数(v0.11.0-preview)

模板属于错误目录,业务只传具体值。参数用有序列表声明,顺序就是生成函数的参数顺序;每个参数均为必传,example 只用于文档。

errorCatalog CommonErrors (
    defaultStatus: 200
    errors: {
        "TextTooLong": {
            "key": "TEXT_TOO_LONG",
            "code": 400002,
            "message": "{label}不能超过{max}个字符",
            "params": [
                {"name": "label", "type": "string", "example": "企业名称"},
                {"name": "max", "type": "int", "example": 100}
            ]
        }
    }
)

与数字码匹配的公共响应及路由声明见完整 errorParams.api。生成后:

apierrors.TextTooLong("企业名称", 100) // Message:企业名称不能超过100个字符
apierrors.TextTooLong("备注", 500)    // Message:备注不能超过500个字符
apierrors.UsernameInvalid()         // 原有固定错误保持无参数

生成函数在返回前完成解析,非 HTTP Service、任务或 CLI 同样能直接读取完整 Message。调用 .WithCause(err) 保留内部原因及 errors.Is/As 错误链,不自动把原因加入提示。每次返回新对象,不能把可变错误保存成共享单例。

运行时入口使用同一个解析器:

err := omnierrors.ByCode("TEXT_TOO_LONG").WithParams(map[string]any{
    "label": "企业名称",
    "max":   100,
})
// HTTP:将 err 返回,当前 App/路由目录负责解析。
// 非 HTTP:使用项目自己的目录显式解析。
registry := omnierrors.MustRegistry(apierrors.Catalog())
resolved := registry.Resolve(err)
fmt.Println(resolved.Message) // 企业名称不能超过100个字符

Go 配置中的 Definition.Params 是 []omnierrors.Parameter,JSON/YAML 使用相同的 params 数组。目录在初始化时复制并编译模板;WithParams 返回新错误并复制参数,之后修改原 map 不会改变该错误。重复解析、路由策略和 Context 缓冲克隆保留参数。

规则如下:

  • 参数名使用 lowerCamelCase,并且不是 Go 关键字。顺序是调用 API 的一部分,重排或变更类型需要同步调整调用方。
  • 支持 string、bool、int、int32、int64、float64。运行时允许整数类型之间按声明范围归一化,拒绝将字符串或浮点数隐式转成整数;float64 要求有限数值。int 范围随 Go 目标平台变化,需要固定位宽时选 int32/int64。
  • 只接受标量,不接受 nil、切片、map、指针、任意对象或 Stringer。通过 JSON 构建运行时参数时使用 Decoder.UseNumber(),避免丢失大整数精度。
  • 模板使用 {name};同一参数可重复出现。{{ / }} 表示字面花括号。插入值不会再次解析,即使 label 本身包含 {max},也只是普通文本。
  • 省略 params 表示旧固定提示,字面花括号不变。不接受空 params 数组;重复参数、未声明占位符、未使用参数、不匹配的花括号、非法类型及错误的 example 会在目录加载/契约校验时失败。
  • 运行时缺参、多传参数、类型或范围错误转为系统错误兜底;不会将原始模板或参数值作为诊断发给客户端。内部 cause 中可通过 errors.As 获取 *omnierrors.ParamError,访问日志带 errorParameter 诊断,不自动记录参数值。
  • 项目可以配置固定的 INTERNAL_ERROR 兜底;若它自身也需要参数且无法解析,则回退框架固定 500,避免递归失败。
  • 模板错误的提示只放目录中;同标识再配置旧 messages 会被拒绝。响应字段、HTTP 状态、data:null 仍由项目策略决定,例如 dataMode: always 保留空 data。

OpenAPI 的 x-omni-errors 保留模板和有序参数定义,响应 examples 使用同一个解析器生成具体文案。未提供 example 时使用稳定样例(string 使用参数名,bool 使用 false,数值使用 1);这些样例不充当运行时默认值,也不是新增 HTTP 请求参数。

幂等重放保存当次已解析的公开错误快照,保留原响应与失败信息。目录更新后,仍在有效期内的重放不会被新文案覆盖;快照不持久化原始参数或内部 cause。整数业务码以不受 JSON 中间层浮点转换影响的方式保存。旧缓存仅保留已有错误标识,不能补推当时缺失的提示与业务码。共享存储的错误快照另受 MaxResponseBytes 限额约束,超限保留不可重放记录,避免重复执行业务。

特殊协议与失败信息

response.WithErrors(catalog, encoder) / MustWithErrors 可绑定路由专用策略,再通过 HandleWithResponse 注册。内置模板校验目录码类型与旧映射;自定义 Encoder 自行决定最终 HTTP 状态、媒体类型和字段结构,可将失败转换成第三方要求的文本/XML 回执。一个 Encoder 作用域不叠加不同目录;外层空目录或相同目录不会覆盖已有目录。

Encoder 的 response.Result 包含逻辑 Code、对外 BusinessCode、公开 Message、原始 Err 及 Failed。使用 result.IsError() 判断失败,不要仅判断 Status >= 400。RawJSON 仅让成功结果保持未包装,HTTP 200 的失败仍使用选定错误策略。c.Text / c.XML / c.JSON 等直接输出及已提交的标准 Handler 响应不会被目录重新包装。

启用访问日志时,记录最终写出的 HTTP 状态,并保留 failed、errorKey 和可用的 businessCode。内置指标将已处理的业务失败计入 Errors;链路记录保留 HTTP 200 和业务失败标记。标准中间件桥接和本地/共享幂等重放保留失败标识;已有缓存条目没有新增元数据时无法追溯识别业务失败,等待正常过期即可。

未知的 ByCode 标识、普通 error、panic 默认走 INTERNAL_ERROR,公开兜底不暴露 cause。项目可显式配置 INTERNAL_ERROR 条目,但目录 DefaultStatus 本身不会改变系统兜底。启用访问日志后,可通过原始 error 追查未知标识和内部原因。

升级

go get git.yuntuo.hk/k2safe/go-omni@v0.11.1-preview
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.11.1-preview
go-omni api generate --dry-run
go-omni api generate
go test ./...

先升级框架依赖,再用同版 CLI 生成;新生成代码最低依赖 v0.11.0-preview。原来的错误构造函数、旧 error (...)、Codes/Messages 与已生成代码可继续运行。把业务中散落的状态/提示逐步替换为生成函数属于项目迁移工作,框架不会自动改写业务逻辑。