errors — 错误模型

一个错误由三部分组成:Kind(映射成 HTTP 状态)、业务 code(客户端用来分支的常量)、 消息(给人的话)。这个示例把三者的每一种组合都跑一遍,你会看到错误在穿越 fmt.Errorf 包装后仍然能被识别,而数据库的原始报错永远进不了响应体。

运行

go run ./examples/errors

纯库演示:没有外部依赖,不监听端口,也不需要 make dev。

它演示了什么

Kind 到状态码

下面这段来自 examples/errors/main.go,它构造了全部九个 Kind 并逐个打印 HTTPStatus()。这张表就是 HTTP 层的映射依据。

fmt.Println("Kind                → HTTP")
for _, c := range kinds() {
	fmt.Printf("  %-18s → %d\n", c.name, c.kind.HTTPStatus())
}

// ── 2. 快捷构造 + Status 提取 ─────────────────────────────────────
fmt.Println("\nerrs.Status(err) → (status, code, msg)")
for _, err := range constructors() {
	status, code, msg := errs.Status(err)
	fmt.Printf("  %-3d %-6d %s\n", status, code, msg)
}

它打印出的九行对应关系是:400 / 401 / 403 / 404 / 409 / 429 / 500 / 503 / 504。

包装后取出的仍然是同一件事

这段同样来自 examples/errors/main.go。要点有两个:普通的 fmt.Errorf("handler: %w", base) 也能被穿透; 而 *errs.Error 上的 Cause 字段是留给日志的, 不出现在客户端拿到的 msg 里。

base := errs.Wrap(errs.KindNotFound, 40401, errors.New("sql: no rows in result set"), "user 42 not found")
wrapped := fmt.Errorf("handler: %w", base) // 普通 %w 包装也能被穿透
status, code, msg := errs.Status(wrapped)
fmt.Printf("  穿透 fmt.Errorf: status=%d code=%d msg=%q\n", status, code, msg)
if e, ok := errors.AsType[*errs.Error](wrapped); ok {
	fmt.Printf("  取出 *errs.Error: Kind=%d Cause=%v(只进日志)\n", e.Kind, e.Cause)
}

dbErr := errs.Wrap(errs.KindConflict, 40901,
	errors.New(`pq: duplicate key value violates unique constraint "users_name_key"`), "创建用户失败")
status, code, msg = errs.Status(dbErr)
fmt.Printf("  Wrap(cause)    : Error()=%q\n", dbErr.Error())

验证

示例不提供 HTTP 接口,看标准输出即可。形状如下(取自 main.go 的打印格式):

Kind                → HTTP
  invalid_argument   → 400
  unauthorized       → 401
  forbidden          → 403
  not_found          → 404
  conflict           → 409
  rate_limited       → 429
  internal           → 500
  unavailable        → 503
  timeout            → 504

errs.Status(err) → (status, code, msg)
  400 40001  name is required
  401 40101  token expired
  ...

包装(errs.Wrap / fmt.Errorf %w):
  穿透 fmt.Errorf: status=404 code=40401 msg="user 42 not found"
  取出 *errs.Error: Kind=4 Cause=sql: no rows in result set(只进日志)
  Wrap(cause)    : Error()="创建用户失败: pq: duplicate key value violates unique constraint \"users_name_key\""
                   对客户端只回: status=409 code=40901 msg="创建用户失败"

code=0 时回退为 状态码*100:
  KindConflict, code=0 → code=40900 msg="已存在"

非 matex 错误:
  status=500 code=50000 msg="internal error"

这里最值得记住的是最后两行:code 传 0 时回退为 状态码 × 100(409 → 40900),而普通错误被兜成 500 / 50000 / "internal error"——"internal detail nobody should see" 这句话没有出现在响应里。

flowchart LR
    E["任意 error"] --> Q{"是 *errs.Error 吗?
(穿透 %w 链)"} Q -- 是 --> K["取 Kind
→ HTTP 状态"] K --> C{"code 为 0?"} C -- 是 --> F["code = 状态码 × 100"] C -- 否 --> U["用业务 code"] Q -- 否 --> D["500 / 50000
internal error"] F --> R["响应
{code, msg, data:null}"] U --> R D --> R X["cause(SQL、内部细节)"] -.->|只进日志| L["日志"] classDef bad fill:#fdf0f0,stroke:#a33,color:#2b2722 class X,L bad
errs.Status 的全部决策。cause 有一条独立的出口通向上游日志, 从不进入响应体。

测试

main_test.go 断言的是这些具体事实:

go test ./examples/errors

相关

错误模型概念页 Error 的结构、构造方式与在 handler 里的用法。 示例索引 全部 20 个可运行示例。

相关示例