Files

257 lines
9.1 KiB
Markdown
Raw Permalink Normal View History

# errorx
把「业务错误码 + 多语言文案 + 占位符」封装成**不可变**的错误常量,并补齐 Go 错误处理的常用能力。
```go
var ErrUserNotFound = errorx.New("user_not_found", 10001, "用户不存在")
err := ErrUserNotFound.Wrap(sql.ErrNoRows) // 保留底层错误,不泄露给用户
fmt.Println(err.GetCode(), err.Msg(ctx)) // 10001 用户不存在
fmt.Println(errors.Is(err, ErrUserNotFound), errors.Is(err, sql.ErrNoRows)) // true true
```
## 特性
| 能力 | 说明 |
| --- | --- |
| 一行定义 | `errorx.New(key, code, defaultValue)` |
| 多语言 | 文案交给 langx,支持 ctx 传语言、固定语言、未注册语言回退 |
| 并发安全 | 错误值不可变,包级常量可被任意 goroutine 并发使用 |
| 不丢上下文 | `Wrap` 保留底层错误,`errors.Is / As / Unwrap` 全部可用 |
| 可排查 | 创建错误时抓调用栈,日志用 `%+v` 直接打印原因与栈 |
| 不伪装成功 | 未注册的 key 返回 500 + 原始 key,绝不返回 langx 的默认码 200 |
## 安装
```bash
go get github.com/yuninks/errorx
```
## 快速开始
```go
package main
import (
"context"
"errors"
"fmt"
"github.com/yuninks/errorx"
"github.com/yuninks/langx"
)
// 1) 定义错误常量:key + 业务码 + 默认文案(占位符写成 #name#)
var (
ErrUserNotFound = errorx.New("user_not_found", 10001, "用户不存在")
ErrParamInvalid = errorx.New("param_invalid", 10002, "参数错误: #field#")
)
func init() {
// 2) 补充其它语言的文案;默认语言(zh)的文案 errorx.New 已经注册好了
langx.AppendTrans("en", map[string]string{
"user_not_found": "user not found",
"param_invalid": "invalid parameter: #field#",
})
}
// 业务层:只管 return error
func loadUser(ctx context.Context, id string) error {
if id == "" {
return ErrParamInvalid.ErrKV("field", "id")
}
if id == "404" {
return ErrUserNotFound.Wrap(errors.New("sql: no rows in result set"))
}
return nil
}
func main() {
ctx := langx.SetCtxLang(context.Background(), "en")
err := loadUser(ctx, "")
var ei errorx.ErrorInterface
if errors.As(err, &ei) {
fmt.Println(ei.GetCode(), ei.Msg(ctx)) // 10002 invalid parameter: id
}
fmt.Println(errors.Is(err, ErrParamInvalid)) // true
}
```
更多可运行示例:
```bash
go run ./example # 基础用法:定义、占位符、多语言、包装、堆栈
go run ./example/http # HTTP 服务:业务层 return error,出口统一转 {code, msg}
go run ./example/imports # langx 资源加载的三种方式与 errorx 的配合
```
## 核心概念
- **ErrorLanguage**:错误常量,`errorx.New` 的返回值。不可变的值类型,可直接作为包级变量。
它本身实现了 `error`,所以 `var err error = ErrUserNotFound` 成立,`errors.Is(err, ErrUserNotFound)` 按 key 比较。
- **ErrorInterface**:错误值接口,`Err() / ErrWith() / Wrap()` 返回它;它本身就是 `error`,取 code 不需要类型断言。
### 错误值是不可变的
所有 `WithXxx` 都返回新值,`GetFormat()` 返回副本,派生不会影响原值,也不需要加锁:
```go
base := ErrParamInvalid.Err()
a := base.WithFormatKV("field", "age")
b := base.WithFormatKV("field", "name")
fmt.Println(a, b, base)
// 参数错误: age / 参数错误: name / 参数错误: #field#
```
> 旧版的 `SetFormat / SetFormatKV / SetLang / SetCtx` 已删除:它们会直接修改共享的错误常量,
> 并发场景下会 `fatal error: concurrent map writes`,并且会让不同请求之间互相改写文案。
### 渲染规则
`Msg(ctx)` 依次尝试:固定语言(`WithLang`)→ ctx 里的语言 → 默认语言 → 错误常量自带的默认文案(回填进 langx)。
任何一步失败都不会 panic,也不会返回裸 key;ctx 为 nil 或 `Accept-Language` 不是字符串同样安全。
## API 速查
**定义错误常量**
| 函数 | 说明 |
| --- | --- |
| `errorx.New(key, code, defaultValue)` | 定义错误常量,key 为空会 panic |
**构造错误值**
| 方法 | 说明 |
| --- | --- |
| `Err()` | 生成错误值 |
| `ErrWith(map)` / `ErrKV(k, v, ...)` | 带占位符数据 |
| `Wrap(cause)` | 包装底层错误,文案里不会出现它 |
| `errorx.NewError(ctx, key)` | 从 langx 资源构造(code 与文案都来自资源) |
| `errorx.NewErrorWith(ctx, key, format)` | 同上,带占位符 |
**读取与渲染**
| 方法 | 说明 |
| --- | --- |
| `GetCode() / GetKey() / GetDefault() / GetFormat() / GetLang()` | 只读访问器 |
| `Msg(ctx) / MsgWith(ctx, map) / MsgKV(ctx, k, v)` | 渲染文案 |
| `Unwrap() / StackTrace()` | 底层错误 / 调用栈 |
**派生(返回新值)**:`WithFormat` / `WithFormatKV` / `WithLang` / `Wrap`
> 命名只有两种后缀:`With(map)` 与 `KV(k, v, ...)`。常量上的 `Err*` 只负责"造错误",造出来的值用 `With*` 派生。
**内置常量**:`Success`(200) / `Error`(400) / `ErrWithMsg`(400) / `ErrInternal`(500) / `ErrUnregistered`(500)
## 多语言
- **语言来源**:`langx.SetCtxLang(ctx, "en")`,或 ctx 里的 `Accept-Language`(langx 约定),见 `example/http`。
- **资源加载**:`AppendCode / AppendTrans`(追加)、`RegisterEmbed`(打包)、`RegisterDir`(目录),见 `example/imports`。
> **注意两种注册语义(langx v0.1.0 起):**
> - `RegisterCode` / `RegisterTrans`:**整体替换**对应的注册表,会把 `errorx.New` 在 init 阶段追加的内置文案冲掉;
> - `RegisterEmbed` / `RegisterDir` / `RegisterFS`:**按键合并**,不影响已注册的资源。
>
> 即使被整体替换,错误常量自带 code 与默认文案,查不到时会自动回填,不会退化成裸 key;
> 但直接用 `langx.GetTransFormat` 读这些 key 仍会看到裸 key —— 业务代码建议统一用 `Append*`。
## 用资源文件定义错误
code 与文案都放在 json 资源里时,用 `NewError / NewErrorWith` 动态构造:
```go
langx.AppendCode(map[string]int{"biz_fail": 1001})
langx.AppendTrans("zh", map[string]string{"biz_fail": "业务失败: #msg#"})
err := errorx.NewErrorWith(ctx, "biz_fail", map[string]string{"msg": "磁盘满了"})
fmt.Println(err.GetCode(), err) // 1001 业务失败: 磁盘满了
```
key 打错时不会静默变成成功:
```go
err := errorx.NewError(ctx, "typo_key")
fmt.Println(err.GetCode(), err) // 500 未注册的错误: typo_key
```
(langx 的 `GetCode` 对未知 key 返回默认码;errorx 用 `langx.HasCode` / `langx.Has` 精确判断,
key 没注册就返回 500,资源里只有文案没有 code 时同样用 500 兜底,不会出现"打错 key 反而成功"。)
## 在 HTTP 服务里使用
```go
func toResponse(ctx context.Context, err error) Response {
var ei errorx.ErrorInterface
if errors.As(err, &ei) {
return Response{Code: ei.GetCode(), Msg: ei.Msg(ctx)}
}
return Response{Code: errorx.ErrInternal.GetCode(), Msg: errorx.ErrInternal.Msg(ctx)}
}
```
业务层只 `return error`,出口层统一翻译,见 `example/http`。
## 日志
```go
log.Printf("%+v", err)
// 数据库繁忙
// cause: dial tcp 127.0.0.1:3306: connect: connection refused
// stack:
// main.loadUser
// /app/main.go:47
```
## 并发
错误值不可变、包级常量没有共享可变状态,可以放心在任意 goroutine 使用:
```go
go func() { _ = ErrUserNotFound.Wrap(err) }()
go func() { _ = ErrParamInvalid.ErrKV("field", "age").Msg(ctx) }()
```
> `errorx.New` 会写入 errorx 的常量注册表,建议只在包级变量或 `init` 中调用。
## 从旧版本迁移
| 旧写法 | 新写法 |
| --- | --- |
| `val.SetLang("en")` | `val = val.WithLang("en")` |
| `val.SetFormat(m)` | `val = val.WithFormat(m)` |
| `val.SetFormatKV(k, v)` | `val = val.WithFormatKV(k, v)` |
| `val.SetCtx(ctx)` | `val = val.WithLang(langx.GetCtxLang(ctx))` |
| `ErrorWithMsg.Errf(m)` | `ErrWith(m)` |
| `ErrorWithMsg.ErrfKV(k, v)` | `ErrKV(k, v)` |
| `ErrorWithMsg.Msgf(ctx, m)` | `MsgWith(ctx, m)` |
| `errorx.NewLanguage(k, c, d)` | `errorx.New(k, c, d)` |
| `errorx.NewStruct(ctx, k, m)` | `errorx.NewErrorWith(ctx, k, m)` |
| `errorx.NewErrorf(ctx, k, m)` | `errorx.NewErrorWith(ctx, k, m)` |
行为变化:
1. 所有 `Set*` 改为 `With*` 并返回新值;错误值不可变,不再有全局污染与并发崩溃。
2. 错误值不再持有 context,只保存语言标识;渲染时把 ctx 传给 `Msg`。
3. `Err() / ErrWith() / ErrKV()` 返回 `ErrorInterface`(本身就是 `error`),取 code 不再需要类型断言。
4. `ErrorLanguage` 不再实现 `ErrorInterface`:常量的职责只有「定义 + 构造」,读取与派生都用 `Err()` 得到的值。
5. 兼容别名 `Errf / ErrfKV / Msgf / NewLanguage / NewStruct` 与空操作 `Copy()` 已删除,不再保留。
6. 未注册的 key 从「默认码 + 裸 key」变成「500 + 未注册提示」。
## 目录结构
```
errorx.go 错误常量(ErrorLanguage)、New、从资源构造错误、内置常量
interfaces.go ErrorInterface 接口与默认实现(不可变错误值、渲染、包装)
stack.go 调用栈采集与解析
example/ 可运行示例
```
## 测试
```bash
go test ./...
go test -race ./... # Windows 需要先装 gcc
```