refactor!: 错误值改为不可变,修复并发崩溃与兜底缺陷
BREAKING CHANGE: 相对 v0.0.1 的 API 变更 - 删除全部 Set* 方法,改为返回新值的 With*;错误值不可变,包级常量可并发使用 - 错误值不再持有 context,只保存语言标识(WithLang),渲染时再传 ctx - 删除兼容别名 Errf / ErrfKV / Msgf / NewLanguage / NewStruct 与空操作 Copy() - Msgf 改为 MsgWith,NewErrorf 改为 NewErrorWith;ErrorLanguage 不再实现 ErrorInterface 修复 - Copy 浅拷贝共享 format map,并发下 fatal error: concurrent map writes - 包级常量被 Set* 污染,请求之间互相改写错误文案 - langx.RegisterCode / RegisterTrans 覆盖注册表后,运行时错误码退化为默认码、文案退化为裸 key - 未注册 key 静默返回默认码 + 裸 key,现返回 500 + 未注册提示 - NewError(nil, ...) 与非 string 的 Accept-Language 触发 panic 新增 - Wrap / Unwrap / errors.Is 按 key 匹配、调用栈(惰性解析)、%+v 输出 cause 与栈 - 错误常量自带默认文案兜底 + errorx 侧常量注册表(不依赖 langx 可变状态) - 根 README、3 个可运行示例、17 个断言测试(语句覆盖率 97.2%)、基准测试 依赖 - github.com/yuninks/langx v0.0.7 -> v0.1.0
This commit is contained in:
@@ -1,62 +1,230 @@
|
||||
package errorx
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/yuninks/langx"
|
||||
)
|
||||
|
||||
// 定义错误常量
|
||||
|
||||
type ErrorLanguage struct {
|
||||
ErrorInterface
|
||||
}
|
||||
|
||||
// 生成错误常量
|
||||
func NewLanguage(uniKey string, code int, defaultValue string) ErrorLanguage {
|
||||
langx.AppendCode(map[string]int{uniKey: code})
|
||||
langx.AppendTrans(langx.GetDefaultLang(), map[string]string{uniKey: defaultValue})
|
||||
|
||||
l := NewStruct(context.Background(), uniKey, nil)
|
||||
|
||||
return ErrorLanguage{l}
|
||||
}
|
||||
|
||||
// Key生成错误信息
|
||||
func (l ErrorLanguage) Err() error {
|
||||
return l
|
||||
}
|
||||
|
||||
// Key生成错误信息
|
||||
func (l ErrorLanguage) Errf(format map[string]string) error {
|
||||
newLang := l.Copy()
|
||||
newLang.SetFormat(format)
|
||||
return newLang
|
||||
}
|
||||
|
||||
func (l ErrorLanguage) ErrfKV(key, value string) error {
|
||||
newLang := l.Copy()
|
||||
newLang.SetFormatKV(key, value)
|
||||
return newLang
|
||||
}
|
||||
|
||||
// 获取翻译后的错误信息
|
||||
func (l ErrorLanguage) Msg(ctx context.Context) string {
|
||||
newLang := l.Copy()
|
||||
newLang.SetCtx(ctx)
|
||||
return newLang.Error()
|
||||
}
|
||||
|
||||
// 获取翻译后的错误信息
|
||||
func (l ErrorLanguage) Msgf(ctx context.Context, format map[string]string) string {
|
||||
newLang := l.Copy()
|
||||
newLang.SetCtx(ctx)
|
||||
newLang.SetFormat(format)
|
||||
return newLang.Error()
|
||||
}
|
||||
|
||||
var (
|
||||
Success ErrorLanguage = NewLanguage("success", 200, "操作成功")
|
||||
Error ErrorLanguage = NewLanguage("error", 400, "操作失败")
|
||||
ErrWithMsg ErrorLanguage = NewLanguage("error_with_msg", 400, "操作失败: #msg#")
|
||||
)
|
||||
// Package errorx 把"业务错误码 + 多语言文案 + 占位符"封装成不可变的错误常量。
|
||||
//
|
||||
// 基本用法:
|
||||
//
|
||||
// var ErrUserNotFound = errorx.New("user_not_found", 10001, "用户不存在")
|
||||
//
|
||||
// func handle(ctx context.Context) error {
|
||||
// if err := load(); err != nil {
|
||||
// return ErrUserNotFound.Wrap(err) // 保留底层错误,errors.Is / errors.Unwrap 可用
|
||||
// }
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// msg := ErrUserNotFound.Msg(ctx) // 按 ctx 里的语言渲染
|
||||
package errorx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/yuninks/langx"
|
||||
)
|
||||
|
||||
// ErrorLanguage 是一个"错误常量":把错误标识(key)、业务错误码(code)
|
||||
// 和默认文案(defaultValue)绑定在一起。
|
||||
//
|
||||
// 它是不可变的值类型,可以直接作为包级变量并发使用:
|
||||
//
|
||||
// var ErrUserNotFound = errorx.New("user_not_found", 10001, "用户不存在")
|
||||
//
|
||||
// 它只负责"定义"和"构造错误值",读取与派生都在构造出来的 ErrorInterface 上做。
|
||||
// 同时它实现了 error 接口,所以可以:
|
||||
//
|
||||
// var err error = ErrUserNotFound // 直接当错误用
|
||||
// errors.Is(someErr, ErrUserNotFound) // 按 key 比较
|
||||
type ErrorLanguage struct {
|
||||
key string
|
||||
code int
|
||||
defaultValue string
|
||||
}
|
||||
|
||||
// New 定义一个新的错误常量,并把 code 与默认文案注册进 langx 与 errorx 的常量注册表。
|
||||
//
|
||||
// key 为空会 panic —— 这是定义期的编程错误,应该在启动时就暴露。
|
||||
// 默认文案里的占位符使用 #name# 形式(可用 langx.SetReplaceKey 修改)。
|
||||
//
|
||||
// 建议只在包级变量或 init 中调用(它会持续写入注册表);动态场景请用 NewError / NewErrorWith。
|
||||
func New(key string, code int, defaultValue string) ErrorLanguage {
|
||||
if key == "" {
|
||||
panic("errorx: New 的 key 不能为空")
|
||||
}
|
||||
langx.AppendCode(map[string]int{key: code})
|
||||
langx.AppendTrans(langx.GetDefaultLang(), map[string]string{key: defaultValue})
|
||||
l := ErrorLanguage{key: key, code: code, defaultValue: defaultValue}
|
||||
register(l)
|
||||
return l
|
||||
}
|
||||
|
||||
func (l ErrorLanguage) toError() *defaultError {
|
||||
return &defaultError{
|
||||
key: l.key,
|
||||
code: l.code,
|
||||
defaultValue: l.defaultValue,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 构造错误值 ----------
|
||||
|
||||
// Err 生成 errorx 错误值(自身即实现了 error)。
|
||||
func (l ErrorLanguage) Err() ErrorInterface {
|
||||
e := l.toError()
|
||||
e.pcs = captureStack()
|
||||
return e
|
||||
}
|
||||
|
||||
// ErrWith 生成带占位符数据的错误值。
|
||||
func (l ErrorLanguage) ErrWith(format map[string]string) ErrorInterface {
|
||||
return l.Err().WithFormat(format)
|
||||
}
|
||||
|
||||
// ErrKV 生成带占位符数据的错误值:ErrKV("name", "tom")。
|
||||
func (l ErrorLanguage) ErrKV(pairs ...string) ErrorInterface {
|
||||
return l.Err().WithFormat(pairsToMap(pairs))
|
||||
}
|
||||
|
||||
// Wrap 生成包装了底层错误的错误值,底层错误可通过 errors.Unwrap / errors.Is 获取,
|
||||
// 且不会出现在 Msg 文案里。
|
||||
func (l ErrorLanguage) Wrap(cause error) ErrorInterface {
|
||||
return l.Err().Wrap(cause)
|
||||
}
|
||||
|
||||
// ---------- 渲染 ----------
|
||||
|
||||
// Error 实现 error 接口:按默认语言渲染。
|
||||
func (l ErrorLanguage) Error() string { return l.toError().Error() }
|
||||
|
||||
// Msg 按 ctx 中携带的语言渲染文案。
|
||||
func (l ErrorLanguage) Msg(ctx context.Context) string { return l.toError().Msg(ctx) }
|
||||
|
||||
// MsgWith 按 ctx 中携带的语言渲染文案,并替换占位符。
|
||||
func (l ErrorLanguage) MsgWith(ctx context.Context, format map[string]string) string {
|
||||
return l.toError().MsgWith(ctx, format)
|
||||
}
|
||||
|
||||
// MsgKV 是 MsgWith 的键值对写法。
|
||||
func (l ErrorLanguage) MsgKV(ctx context.Context, pairs ...string) string {
|
||||
return l.toError().MsgKV(ctx, pairs...)
|
||||
}
|
||||
|
||||
// ---------- 读取 ----------
|
||||
|
||||
// GetCode 返回业务错误码。
|
||||
func (l ErrorLanguage) GetCode() int { return l.code }
|
||||
|
||||
// GetKey 返回错误标识。
|
||||
func (l ErrorLanguage) GetKey() string { return l.key }
|
||||
|
||||
// GetDefault 返回默认文案。
|
||||
func (l ErrorLanguage) GetDefault() string { return l.defaultValue }
|
||||
|
||||
// Is 让 errors.Is 按错误标识(key)匹配。
|
||||
func (l ErrorLanguage) Is(target error) bool {
|
||||
if l.key == "" || target == nil {
|
||||
return false
|
||||
}
|
||||
if t, ok := target.(interface{ GetKey() string }); ok {
|
||||
return t.GetKey() == l.key
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ---------- 从 langx 资源构造错误 ----------
|
||||
|
||||
// NewError 用 langx 中已注册的 key 构造错误(code 与文案都来自资源文件)。
|
||||
//
|
||||
// key 未注册时返回 ErrUnregistered(code 500)并带上原始 key,
|
||||
// 不会把打错的 key 当成正常错误。
|
||||
func NewError(ctx context.Context, key string) ErrorInterface {
|
||||
return newFromRegistry(ctx, key, nil)
|
||||
}
|
||||
|
||||
// NewErrorWith 是 NewError 的带占位符版本。
|
||||
func NewErrorWith(ctx context.Context, key string, format map[string]string) ErrorInterface {
|
||||
return newFromRegistry(ctx, key, format)
|
||||
}
|
||||
|
||||
func newFromRegistry(ctx context.Context, key string, format map[string]string) ErrorInterface {
|
||||
lang := langx.GetCtxLang(ctx)
|
||||
code, defaultValue, ok := resolve(key)
|
||||
if !ok {
|
||||
return ErrUnregistered.ErrKV("key", key).WithLang(lang)
|
||||
}
|
||||
return &defaultError{
|
||||
key: key,
|
||||
code: code,
|
||||
defaultValue: defaultValue,
|
||||
lang: lang,
|
||||
format: cloneFormat(format),
|
||||
pcs: captureStack(),
|
||||
}
|
||||
}
|
||||
|
||||
// CodeUnknown 是"有文案但没有注册错误码"时使用的兜底码。
|
||||
// 取 500 而不是 langx 的默认码(默认 -1,也可能被改成 200),避免配置缺失被当成成功。
|
||||
const CodeUnknown = 500
|
||||
|
||||
// resolve 按 key 取出业务码与默认文案:
|
||||
//
|
||||
// 1. 优先用 errorx.New 定义过的错误常量(不依赖 langx 的注册表);
|
||||
// 2. 其次用 langx 资源文件(code.json / xx.json)里注册的内容;
|
||||
// 3. 两者都没有时返回 ok=false,由调用方转成 ErrUnregistered。
|
||||
func resolve(key string) (code int, defaultValue string, ok bool) {
|
||||
if key == "" {
|
||||
return 0, "", false
|
||||
}
|
||||
if l, hit := lookupOwn(key); hit {
|
||||
return l.code, l.defaultValue, true
|
||||
}
|
||||
|
||||
lang := langx.GetDefaultLang()
|
||||
hasMsg := langx.Has(lang, key)
|
||||
hasCode := langx.HasCode(key)
|
||||
|
||||
switch {
|
||||
case hasMsg && hasCode:
|
||||
return langx.GetCode(key), langx.GetMsg(lang, key), true
|
||||
case hasMsg:
|
||||
// 资源里只有文案没有 code:保留文案,但用 CodeUnknown 兜底
|
||||
return CodeUnknown, langx.GetMsg(lang, key), true
|
||||
case hasCode:
|
||||
// 资源里只有 code 没有文案:文案退化为 key 本身
|
||||
return langx.GetCode(key), key, true
|
||||
default:
|
||||
return 0, "", false
|
||||
}
|
||||
}
|
||||
|
||||
// errorx 自己的错误常量注册表:即使 langx 的资源被 RegisterCode / RegisterTrans 覆盖,
|
||||
// 用 errorx.New 定义过的常量依然能拿到正确的 code 与默认文案。
|
||||
var (
|
||||
registryMu sync.RWMutex
|
||||
registry = map[string]ErrorLanguage{}
|
||||
)
|
||||
|
||||
func register(l ErrorLanguage) {
|
||||
registryMu.Lock()
|
||||
defer registryMu.Unlock()
|
||||
registry[l.key] = l
|
||||
}
|
||||
|
||||
func lookupOwn(key string) (ErrorLanguage, bool) {
|
||||
registryMu.RLock()
|
||||
defer registryMu.RUnlock()
|
||||
l, ok := registry[key]
|
||||
return l, ok
|
||||
}
|
||||
|
||||
// 内置错误常量。
|
||||
var (
|
||||
// Success 表示成功。
|
||||
Success = New("success", 200, "操作成功")
|
||||
// Error 表示通用失败。
|
||||
Error = New("error", 400, "操作失败")
|
||||
// ErrWithMsg 表示带失败原因的通用失败,文案里的 #msg# 由调用方填充。
|
||||
ErrWithMsg = New("error_with_msg", 400, "操作失败: #msg#")
|
||||
// ErrInternal 用于兜底的非业务错误。
|
||||
ErrInternal = New("errorx_internal", 500, "服务器内部错误")
|
||||
// ErrUnregistered 表示使用了未注册的 key,由 NewError 自动返回。
|
||||
ErrUnregistered = New("errorx_unregistered", 500, "未注册的错误: #key#")
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user