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:
+197
-69
@@ -2,95 +2,223 @@ package errorx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/yuninks/langx"
|
||||
)
|
||||
|
||||
// Key生成错误信息
|
||||
// ErrorInterface 是 errorx 中所有错误值的统一接口。
|
||||
//
|
||||
// 三条设计约束:
|
||||
//
|
||||
// 1. 不可变:所有 WithXxx 方法都返回新的错误值,绝不修改接收者。
|
||||
// 因此包级错误常量可以放心地被任意 goroutine 并发使用。
|
||||
//
|
||||
// 2. 不持有 context:错误值只保存语言标识(WithLang),渲染时再把 ctx 传进来
|
||||
// (Msg / MsgWith / MsgKV)。避免错误值被日志、队列、缓存长期持有的时候
|
||||
// 把整个请求上下文一起拖住。
|
||||
//
|
||||
// 3. 渲染永不 panic:ctx 为 nil、ctx 里的语言类型不对、langx 资源被清空,
|
||||
// 都会安全降级到默认文案,而不是崩溃或返回裸 key。
|
||||
type ErrorInterface interface {
|
||||
Copy() ErrorInterface // 复制一个新的错误信息
|
||||
Error() string // 实现error接口&获取翻译后的错误信息
|
||||
GetCode() int // 获取翻译后的Code
|
||||
GetKey() string // 获取原Key值
|
||||
GetFormat() map[string]string // 获取附加数据
|
||||
SetFormat(format map[string]string) // 设置附加数据
|
||||
SetCtx(ctxHttp context.Context) // 设置上下文
|
||||
SetLang(lang string) // 设置语言
|
||||
SetFormatKV(key, value string) // 设置附加数据键值对
|
||||
error
|
||||
|
||||
// GetCode 返回业务错误码。
|
||||
GetCode() int
|
||||
// GetKey 返回错误常量的唯一标识。
|
||||
GetKey() string
|
||||
// GetDefault 返回定义错误时写入的默认文案。
|
||||
GetDefault() string
|
||||
// GetFormat 返回占位符数据的副本,修改返回值不会影响错误本身。
|
||||
GetFormat() map[string]string
|
||||
// GetLang 返回错误值上固定的语言,空字符串表示跟随渲染时的 ctx。
|
||||
GetLang() string
|
||||
|
||||
// Unwrap 返回被 Wrap 包装的底层错误,用于 errors.Is / errors.As / errors.Unwrap。
|
||||
Unwrap() error
|
||||
// StackTrace 返回创建错误时的调用栈,已过滤 errorx 自身的栈帧。
|
||||
StackTrace() StackTrace
|
||||
|
||||
// WithFormat 返回携带占位符数据的新错误值,传 nil 表示清空占位符。
|
||||
WithFormat(format map[string]string) ErrorInterface
|
||||
// WithFormatKV 返回追加一组占位符数据的新错误值。
|
||||
WithFormatKV(key, value string) ErrorInterface
|
||||
// WithLang 返回固定了语言的新错误值。
|
||||
WithLang(lang string) ErrorInterface
|
||||
// Wrap 返回包装了底层错误的新错误值。
|
||||
Wrap(cause error) ErrorInterface
|
||||
|
||||
// Msg 使用 ctx 中携带的语言渲染文案。
|
||||
Msg(ctx context.Context) string
|
||||
// MsgWith 使用 ctx 中携带的语言渲染文案,并替换占位符。
|
||||
MsgWith(ctx context.Context, format map[string]string) string
|
||||
// MsgKV 是 MsgWith 的键值对写法:MsgKV(ctx, "name", "tom")。
|
||||
MsgKV(ctx context.Context, pairs ...string) string
|
||||
}
|
||||
|
||||
// defaultError 是 ErrorInterface 的默认实现。
|
||||
// 所有字段在构造完成后不再变化,WithXxx 一律返回新实例。
|
||||
type defaultError struct {
|
||||
ctx context.Context
|
||||
key string
|
||||
format map[string]string
|
||||
key string
|
||||
code int
|
||||
defaultValue string
|
||||
format map[string]string // 只读
|
||||
lang string // 固定语言,空串表示跟随渲染时的 ctx
|
||||
cause error
|
||||
pcs []uintptr // 惰性解析的调用栈
|
||||
}
|
||||
|
||||
func (l *defaultError) Copy() ErrorInterface {
|
||||
return &defaultError{
|
||||
ctx: l.ctx,
|
||||
key: l.key,
|
||||
format: l.format,
|
||||
// ---------- 只读访问器 ----------
|
||||
|
||||
func (e *defaultError) GetCode() int { return e.code }
|
||||
|
||||
func (e *defaultError) GetKey() string { return e.key }
|
||||
|
||||
func (e *defaultError) GetDefault() string { return e.defaultValue }
|
||||
|
||||
func (e *defaultError) GetFormat() map[string]string { return cloneFormat(e.format) }
|
||||
|
||||
func (e *defaultError) GetLang() string { return e.lang }
|
||||
|
||||
func (e *defaultError) Unwrap() error { return e.cause }
|
||||
|
||||
func (e *defaultError) StackTrace() StackTrace { return resolveStack(e.pcs) }
|
||||
|
||||
// ---------- 不可变派生 ----------
|
||||
|
||||
func (e *defaultError) clone() *defaultError {
|
||||
cp := *e
|
||||
return &cp
|
||||
}
|
||||
|
||||
func (e *defaultError) WithFormat(format map[string]string) ErrorInterface {
|
||||
cp := e.clone()
|
||||
cp.format = cloneFormat(format)
|
||||
return cp
|
||||
}
|
||||
|
||||
func (e *defaultError) WithFormatKV(key, value string) ErrorInterface {
|
||||
cp := e.clone()
|
||||
cp.format = cloneFormat(e.format)
|
||||
cp.format[key] = value
|
||||
return cp
|
||||
}
|
||||
|
||||
func (e *defaultError) WithLang(lang string) ErrorInterface {
|
||||
cp := e.clone()
|
||||
cp.lang = lang
|
||||
return cp
|
||||
}
|
||||
|
||||
func (e *defaultError) Wrap(cause error) ErrorInterface {
|
||||
cp := e.clone()
|
||||
cp.cause = cause
|
||||
return cp
|
||||
}
|
||||
|
||||
// ---------- 渲染 ----------
|
||||
|
||||
// Error 实现 error 接口:使用错误值上固定的语言,没有则用默认语言。
|
||||
func (e *defaultError) Error() string { return e.render(e.lang) }
|
||||
|
||||
// Msg 使用 ctx 中携带的语言渲染;若错误值用 WithLang 固定了语言,则以固定语言为准。
|
||||
func (e *defaultError) Msg(ctx context.Context) string {
|
||||
if e.lang != "" {
|
||||
return e.render(e.lang)
|
||||
}
|
||||
return e.render(langx.GetCtxLang(ctx))
|
||||
}
|
||||
|
||||
func (e *defaultError) MsgWith(ctx context.Context, format map[string]string) string {
|
||||
return e.WithFormat(format).Msg(ctx)
|
||||
}
|
||||
|
||||
func (e *defaultError) MsgKV(ctx context.Context, pairs ...string) string {
|
||||
return e.WithFormat(pairsToMap(pairs)).Msg(ctx)
|
||||
}
|
||||
|
||||
// render 是唯一真正读取 langx 的地方:langx 里没有对应文案时,
|
||||
// 用错误常量自带的默认文案兜底,绝不返回裸 key。
|
||||
func (e *defaultError) render(lang string) string {
|
||||
if e.key == "" {
|
||||
return emptyKeyMsg
|
||||
}
|
||||
if lang == "" {
|
||||
lang = langx.GetDefaultLang()
|
||||
}
|
||||
if !langx.Has(lang, e.key) {
|
||||
e.registerDefault()
|
||||
}
|
||||
return langx.GetFormat(lang, e.key, e.format)
|
||||
}
|
||||
|
||||
// registerDefault 把默认文案回填到 langx 的默认语言。
|
||||
// 用于用户调用 langx.RegisterTrans / RegisterCode 覆盖资源、
|
||||
// 或者默认语言在 main 里被改掉之后,仍然能拿到可读的文案。
|
||||
func (e *defaultError) registerDefault() {
|
||||
if e.defaultValue == "" || e.defaultValue == e.key {
|
||||
return
|
||||
}
|
||||
langx.AppendTrans(langx.GetDefaultLang(), map[string]string{e.key: e.defaultValue})
|
||||
}
|
||||
|
||||
// Is 让 errors.Is 按错误标识(key)匹配:errors.Is(err, errorx.Error)。
|
||||
func (e *defaultError) Is(target error) bool {
|
||||
return sameKey(e, target)
|
||||
}
|
||||
|
||||
// Format 支持 %v / %s / %q,以及带堆栈和原因的 %+v。
|
||||
func (e *defaultError) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
if !s.Flag('+') {
|
||||
io.WriteString(s, e.Error())
|
||||
return
|
||||
}
|
||||
io.WriteString(s, e.Error())
|
||||
if cause := e.Unwrap(); cause != nil {
|
||||
fmt.Fprintf(s, "\n cause: %+v", cause)
|
||||
}
|
||||
if st := e.StackTrace(); len(st) > 0 {
|
||||
fmt.Fprintf(s, "\n stack:\n%s", st)
|
||||
}
|
||||
case 's':
|
||||
io.WriteString(s, e.Error())
|
||||
case 'q':
|
||||
fmt.Fprintf(s, "%q", e.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (e *defaultError) SetFormat(format map[string]string) {
|
||||
e.format = format
|
||||
}
|
||||
// ---------- 内部工具 ----------
|
||||
|
||||
func (l *defaultError) SetFormatKV(key, value string) {
|
||||
if l.format == nil {
|
||||
l.format = make(map[string]string)
|
||||
const emptyKeyMsg = "errorx: 未定义的错误"
|
||||
|
||||
// cloneFormat 复制占位符数据,保证错误值之间不共享 map。
|
||||
func cloneFormat(src map[string]string) map[string]string {
|
||||
dst := make(map[string]string, len(src))
|
||||
for k, v := range src {
|
||||
dst[k] = v
|
||||
}
|
||||
l.format[key] = value
|
||||
return dst
|
||||
}
|
||||
|
||||
func (e *defaultError) SetCtx(ctxHttp context.Context) {
|
||||
e.ctx = ctxHttp
|
||||
}
|
||||
|
||||
func (l *defaultError) Error() string {
|
||||
errLang := langx.GetCtxLang(l.ctx)
|
||||
return langx.GetFormat(errLang, l.key, l.format)
|
||||
}
|
||||
|
||||
func (e *defaultError) GetCode() int {
|
||||
return langx.GetCode(e.key)
|
||||
}
|
||||
|
||||
func (e *defaultError) GetKey() string {
|
||||
return e.key
|
||||
}
|
||||
|
||||
func (e *defaultError) GetFormat() map[string]string {
|
||||
if e.format == nil {
|
||||
e.format = make(map[string]string)
|
||||
// pairsToMap 把 ("k1","v1","k2","v2") 转成 map;落单的 key 会被忽略。
|
||||
func pairsToMap(pairs []string) map[string]string {
|
||||
m := make(map[string]string, len(pairs)/2)
|
||||
for i := 0; i+1 < len(pairs); i += 2 {
|
||||
m[pairs[i]] = pairs[i+1]
|
||||
}
|
||||
return e.format
|
||||
return m
|
||||
}
|
||||
|
||||
func (e *defaultError) SetLang(lang string) {
|
||||
e.ctx = langx.SetCtxLang(e.ctx, lang)
|
||||
}
|
||||
|
||||
func NewErrorf(ctx context.Context, key string, format map[string]string) error {
|
||||
return &defaultError{
|
||||
ctx: ctx,
|
||||
key: key,
|
||||
format: format,
|
||||
// sameKey 判断两个错误是否描述同一个业务错误。
|
||||
func sameKey(e *defaultError, target error) bool {
|
||||
if e.key == "" || target == nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func NewError(ctx context.Context, key string) error {
|
||||
return &defaultError{
|
||||
ctx: ctx,
|
||||
key: key,
|
||||
}
|
||||
}
|
||||
|
||||
func NewStruct(ctx context.Context, key string, format map[string]string) ErrorInterface {
|
||||
return &defaultError{
|
||||
ctx: ctx,
|
||||
key: key,
|
||||
format: format,
|
||||
if t, ok := target.(interface{ GetKey() string }); ok {
|
||||
return t.GetKey() == e.key
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user