2025-10-26 20:45:21 +08:00
|
|
|
|
package errorx
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
2026-09-21 00:43:43 +08:00
|
|
|
|
"fmt"
|
|
|
|
|
|
"io"
|
2025-10-26 20:45:21 +08:00
|
|
|
|
|
|
|
|
|
|
"github.com/yuninks/langx"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-09-21 00:43:43 +08:00
|
|
|
|
// ErrorInterface 是 errorx 中所有错误值的统一接口。
|
|
|
|
|
|
//
|
|
|
|
|
|
// 三条设计约束:
|
|
|
|
|
|
//
|
|
|
|
|
|
// 1. 不可变:所有 WithXxx 方法都返回新的错误值,绝不修改接收者。
|
|
|
|
|
|
// 因此包级错误常量可以放心地被任意 goroutine 并发使用。
|
|
|
|
|
|
//
|
|
|
|
|
|
// 2. 不持有 context:错误值只保存语言标识(WithLang),渲染时再把 ctx 传进来
|
|
|
|
|
|
// (Msg / MsgWith / MsgKV)。避免错误值被日志、队列、缓存长期持有的时候
|
|
|
|
|
|
// 把整个请求上下文一起拖住。
|
|
|
|
|
|
//
|
|
|
|
|
|
// 3. 渲染永不 panic:ctx 为 nil、ctx 里的语言类型不对、langx 资源被清空,
|
|
|
|
|
|
// 都会安全降级到默认文案,而不是崩溃或返回裸 key。
|
2025-10-26 20:45:21 +08:00
|
|
|
|
type ErrorInterface interface {
|
2026-09-21 00:43:43 +08:00
|
|
|
|
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
|
2025-10-26 20:45:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-21 00:43:43 +08:00
|
|
|
|
// defaultError 是 ErrorInterface 的默认实现。
|
|
|
|
|
|
// 所有字段在构造完成后不再变化,WithXxx 一律返回新实例。
|
2025-10-26 20:45:21 +08:00
|
|
|
|
type defaultError struct {
|
2026-09-21 00:43:43 +08:00
|
|
|
|
key string
|
|
|
|
|
|
code int
|
|
|
|
|
|
defaultValue string
|
|
|
|
|
|
format map[string]string // 只读
|
|
|
|
|
|
lang string // 固定语言,空串表示跟随渲染时的 ctx
|
|
|
|
|
|
cause error
|
|
|
|
|
|
pcs []uintptr // 惰性解析的调用栈
|
2025-10-26 20:45:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-21 00:43:43 +08:00
|
|
|
|
// ---------- 只读访问器 ----------
|
|
|
|
|
|
|
|
|
|
|
|
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())
|
2025-10-26 20:45:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-21 00:43:43 +08:00
|
|
|
|
// ---------- 内部工具 ----------
|
2025-10-26 20:45:21 +08:00
|
|
|
|
|
2026-09-21 00:43:43 +08:00
|
|
|
|
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
|
2025-10-26 20:45:21 +08:00
|
|
|
|
}
|
2026-09-21 00:43:43 +08:00
|
|
|
|
return dst
|
2025-10-26 20:45:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-21 00:43:43 +08:00
|
|
|
|
// 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]
|
2025-10-26 20:45:21 +08:00
|
|
|
|
}
|
2026-09-21 00:43:43 +08:00
|
|
|
|
return m
|
2025-10-26 20:45:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-21 00:43:43 +08:00
|
|
|
|
// sameKey 判断两个错误是否描述同一个业务错误。
|
|
|
|
|
|
func sameKey(e *defaultError, target error) bool {
|
|
|
|
|
|
if e.key == "" || target == nil {
|
|
|
|
|
|
return false
|
2025-10-26 20:45:21 +08:00
|
|
|
|
}
|
2026-09-21 00:43:43 +08:00
|
|
|
|
if t, ok := target.(interface{ GetKey() string }); ok {
|
|
|
|
|
|
return t.GetKey() == e.key
|
2025-10-26 20:45:21 +08:00
|
|
|
|
}
|
2026-09-21 00:43:43 +08:00
|
|
|
|
return false
|
2025-10-26 20:45:21 +08:00
|
|
|
|
}
|