diff --git a/error_test.go b/error_test.go index 574ce0a..9b2708a 100644 --- a/error_test.go +++ b/error_test.go @@ -8,11 +8,7 @@ import ( "github.com/yuninks/langx" ) -func TestError(t *testing.T) { - var err error - - ctx := context.Background() - +func init() { langx.InitLangx( langx.SetDefaultCode(0), langx.SetDefaultLanguage("zh"), @@ -21,39 +17,144 @@ func TestError(t *testing.T) { "login_success": 200, "error": 400, "username": 201, + "password": 202, }) langx.RegisterTrans("zh", map[string]string{ - "login_success": "成功", + "login_success": "登录成功", "error": "错误", - "username": "你好 #name#", // 有占位符 + "username": "你好 #name#", + "password": "密码错误: #reason#", }) langx.RegisterTrans("en", map[string]string{ - "login_success": "success", - "error": "error", - "username": "Hello #name#", // 有占位符 + "login_success": "Login success", + "error": "Error", + "username": "Hello #name#", + "password": "Password error: #reason#", }) +} - err = errorx.NewError(ctx, "error") - // fmt.Printf("err: %v\n", err) - t.Log(err.Error()) - val, ok := err.(errorx.ErrorInterface) - if ok { - t.Log(val.GetCode()) +func TestNewError_Basic(t *testing.T) { + ctx := context.Background() + + err := errorx.NewError(ctx, "error") + t.Log(err.Error()) // 输出:错误 + + if code := err.Code(); code != 400 { + t.Fatalf("expected code 400, got %d", code) } +} - err = errorx.NewErrorf(ctx, "username", map[string]string{ +func TestNewErrorf_Placeholder(t *testing.T) { + ctx := context.Background() + + err := errorx.NewErrorf(ctx, "username", map[string]string{ "name": "yuninks", }) - t.Log(err.Error()) - val, ok = err.(errorx.ErrorInterface) - if ok { - t.Log(val.GetCode()) + t.Log(err.Error()) // 输出:你好 yuninks - // 设置输出语言 - val.SetLang("en") + // 不可变:WithLang 返回新实例,原实例不变 + errEn := err.WithLang("en") + t.Log(err.Error()) // 仍然是中文 + t.Log(errEn.Error()) // 输出:Hello yuninks +} +func TestErrorCode_Predefined(t *testing.T) { + ctx := context.Background() + + // 使用预定义 ErrorCode 创建运行时错误 + err := errorx.Error.New(ctx) + t.Log(err.Error()) // 输出:操作失败 + if code := err.Code(); code != 400 { + t.Fatalf("expected code 400, got %d", code) + } +} + +func TestErrorCode_Newf(t *testing.T) { + ctx := context.Background() + + err := errorx.ErrWithMsg.Newf(ctx, map[string]string{"msg": "参数校验失败"}) + t.Log(err.Error()) // 输出:操作失败: 参数校验失败 +} + +func TestErrorCode_Msg(t *testing.T) { + ctx := context.Background() + + msg := errorx.Success.Msg(ctx) + t.Log(msg) // 输出:操作成功 + if msg != "操作成功" { + t.Fatalf("expected '操作成功', got '%s'", msg) } - t.Log(val.Error()) + // 验证中文环境 + if code := errorx.Success.Code(); code != 200 { + t.Fatalf("expected code 200, got %d", code) + } +} +func TestHelper_As(t *testing.T) { + ctx := context.Background() + + err := errorx.NewError(ctx, "username") + e, ok := errorx.As(err) + if !ok { + t.Fatal("expected errorx.As to succeed") + } + t.Log(e.Key()) // username + t.Log(e.Code()) // 201 +} + +func TestHelper_CodeFrom(t *testing.T) { + ctx := context.Background() + + err := errorx.NewError(ctx, "error") + if code := errorx.CodeFrom(err); code != 400 { + t.Fatalf("expected 400, got %d", code) + } + + // 非 langError 返回 -1 + if code := errorx.CodeFrom(context.Canceled); code != -1 { + t.Fatalf("expected -1 for non-langError, got %d", code) + } +} + +func TestHelper_KeyFrom(t *testing.T) { + err := errorx.NewError(context.Background(), "login_success") + if key := errorx.KeyFrom(err); key != "login_success" { + t.Fatalf("expected 'login_success', got '%s'", key) + } +} + +func TestImmutability_CreateChain(t *testing.T) { + // 演示不可变链式调用 + ctx := context.Background() + + base := errorx.Error.New(ctx) + zh := base.WithKV("msg", "中文错误") + en := zh.WithLang("en").WithKV("msg", "English error") + + t.Log(base.Error()) // 操作失败 + t.Log(zh.Error()) // 操作失败: 中文错误 + t.Log(en.Error()) // 操作失败: English error + + // 各自独立,互不影响 + zh2 := zh.WithKV("extra", "additional") + t.Log(zh.Error()) // 仍然只有 msg + t.Log(zh2.Error()) // msg + extra 都有了 +} + +func TestConcurrency_Safety(t *testing.T) { + // 多个 goroutine 共享同一个 ErrorCode,各自派生独立实例 + ctx := context.Background() + done := make(chan bool, 10) + + for range 10 { + go func() { + _ = errorx.Error.New(ctx).WithKV("msg", "go routine") + done <- true + }() + } + for range 10 { + <-done + } + // 无竞态即为通过 } diff --git a/errorx.go b/errorx.go index 392fb5f..d44a8bb 100644 --- a/errorx.go +++ b/errorx.go @@ -1,62 +1,58 @@ -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 + +import ( + "context" + + "github.com/yuninks/langx" +) + +// ErrorCode 表示一个预定义的错误码,是纯值类型,无状态、并发安全。 +// 声明为 var 后不会被意外修改。 +type ErrorCode struct { + key string + code int +} + +// NewCode 创建一个错误码,并自动注册到 langx 全局表中。 +// - key: 唯一标识符 +// - code: 业务错误码 +// - defaultMsg: 默认语言下的消息模板 +func NewCode(key string, code int, defaultMsg string) ErrorCode { + langx.AppendCode(map[string]int{key: code}) + langx.AppendTrans(langx.GetDefaultLang(), map[string]string{key: defaultMsg}) + return ErrorCode{key: key, code: code} +} + +// Key 返回唯一标识符。 +func (ec ErrorCode) Key() string { return ec.key } + +// Code 返回业务错误码。 +func (ec ErrorCode) Code() int { return ec.code } + +// New 创建一个携带上下文的运行时错误。 +func (ec ErrorCode) New(ctx context.Context) *langError { + return NewError(ctx, ec.key) +} + +// Newf 创建一个携带占位符键值对的运行时错误。 +func (ec ErrorCode) Newf(ctx context.Context, kv map[string]string) *langError { + return NewErrorf(ctx, ec.key, kv) +} + +// Msg 直接获取当前上下文语言下的翻译消息。 +func (ec ErrorCode) Msg(ctx context.Context) string { + return langx.GetFormat(langx.GetCtxLang(ctx), ec.key, nil) +} + +// Msgf 直接获取带占位符替换的翻译消息。 +func (ec ErrorCode) Msgf(ctx context.Context, kv map[string]string) string { + return langx.GetFormat(langx.GetCtxLang(ctx), ec.key, kv) +} + +// ---- 预定义错误码 ---------------------------------------------------- + +var ( + Success = NewCode("success", 200, "操作成功") + Error = NewCode("error", 400, "操作失败") + ErrWithMsg = NewCode("error_with_msg", 400, "操作失败: #msg#") +) diff --git a/example/enample2.go b/example/enample2.go index 3ca431d..774ae64 100644 --- a/example/enample2.go +++ b/example/enample2.go @@ -2,48 +2,237 @@ package main import ( "context" + "fmt" + "net/http" "github.com/yuninks/errorx" "github.com/yuninks/langx" ) -func main() { - - err := ErrorWithMsg.Error() - - // 输出:错误 - println(err.Error()) - - err = ErrorWithMsg.Errorf(map[string]string{"msg": "错误"}) - // 输出:错误 - println(err.Error()) - -} - -type Language string - -// 添加key+默认语言 -func newLanguage(uniKey string, code int, defaultValue string) Language { - langx.AppendCode(map[string]int{uniKey: code}) - langx.AppendTrans("zh_hans", map[string]string{uniKey: defaultValue}) - return Language(uniKey) -} - -func (l Language) String() string { - return string(l) -} - -func (l Language) Error() error { - return errorx.NewError(context.Background(), l.String()) -} - -func (l Language) Errorf(format map[string]string) error { - return errorx.NewErrorf(context.Background(), l.String(), format) -} +// ---- 定义业务错误码 --------------------------------------------------- +// 预定义错误码,集中声明,类型安全。 var ( - Success Language = newLanguage("success", 200, "成功") - - Error Language = newLanguage("error", 400, "错误") - ErrorWithMsg Language = newLanguage("error_with_msg", 400, "错误 #msg#") + ErrLoginFailed = errorx.NewCode("login_failed", 401, "登录失败") + ErrTokenExpired = errorx.NewCode("token_expired", 401, "令牌已过期,请重新登录") + ErrUserNotFound = errorx.NewCode("user_not_found", 404, "用户 #name# 不存在") + ErrParamInvalid = errorx.NewCode("param_invalid", 422, "参数校验失败: #field#") + ErrInternal = errorx.NewCode("internal", 500, "服务器内部错误") + ErrRateLimit = errorx.NewCode("rate_limit", 429, "请求过于频繁,请 #seconds# 秒后重试") ) + +// ---- 补充语言包(可通过 embed/文件 批量导入) -------------------------- + +func init() { + langx.RegisterTrans("en", map[string]string{ + "login_failed": "Login failed", + "token_expired": "Token expired, please re-login", + "user_not_found": "User #name# not found", + "param_invalid": "Parameter validation failed: #field#", + "internal": "Internal server error", + "rate_limit": "Too many requests, retry in #seconds# seconds", + }) +} + +// ====================================================================== +// 示例 1:基础用法 — 创建错误并获取多语言消息 +// ====================================================================== +func exampleBasic() { + fmt.Println("=== 示例1:基础用法 ===") + + ctx := context.Background() + + // 创建带上下文的运行时错误 + err := ErrLoginFailed.New(ctx) + fmt.Println("中文:", err.Error()) + + // 切换到英文 + errEn := err.WithLang("en") + fmt.Println("英文:", errEn.Error()) + + // 原实例不受影响 + fmt.Println("原实例仍是中文:", err.Error()) + + // 直接从 ErrorCode 获取翻译消息(不创建 error) + fmt.Println("直接翻译:", ErrLoginFailed.Msg(ctx)) + fmt.Println() +} + +// ====================================================================== +// 示例 2:占位符替换 +// ====================================================================== +func examplePlaceholder() { + fmt.Println("=== 示例2:占位符替换 ===") + + ctx := context.Background() + + // 使用 Newf 创建带占位符的错误 + err := ErrUserNotFound.Newf(ctx, map[string]string{"name": "admin"}) + fmt.Println("中文:", err.Error()) + + // 链式派生:追加更多占位符 + err2 := err.WithKV("extra", "value") + fmt.Println("追加占位符:", err2.Error()) + // err 不受影响 + fmt.Println("原实例不变:", err.Error()) + + // 不可变链式:中文 → 英文 + errEn := ErrRateLimit. + Newf(ctx, map[string]string{"seconds": "30"}). + WithLang("en") + fmt.Println("英文:", errEn.Error()) + + // 批量替换 WithMap + err3 := ErrParamInvalid.New(ctx).WithMap(map[string]string{ + "field": "email", + }) + fmt.Println("WithMap:", err3.Error()) + fmt.Println() +} + +// ====================================================================== +// 示例 3:辅助函数 — 从 error 中提取信息 +// ====================================================================== +func exampleHelpers() { + fmt.Println("=== 示例3:辅助函数 ===") + + ctx := context.Background() + err := ErrParamInvalid.Newf(ctx, map[string]string{"field": "age"}) + + // 类型安全提取 + e, ok := errorx.As(err) + if ok { + fmt.Println("Key:", e.Key()) + fmt.Println("Code:", e.Code()) + fmt.Println("Format:", e.Format()) + } + + // 快捷函数 + fmt.Println("CodeFrom:", errorx.CodeFrom(err)) + fmt.Println("KeyFrom:", errorx.KeyFrom(err)) + + // 对非 langError 的容错处理 + fmt.Println("CodeFrom(context.Canceled):", errorx.CodeFrom(context.Canceled)) // -1 + fmt.Println() +} + +// ====================================================================== +// 示例 4:HTTP API 响应 — 模拟真实场景 +// ====================================================================== +func exampleHTTP() { + fmt.Println("=== 示例4:HTTP API 响应 ===") + + // 模拟从请求中获取语言 + zhCtx := langx.SetCtxLang(context.Background(), "zh") + enCtx := langx.SetCtxLang(context.Background(), "en") + + // 处理请求 + handleLogin := func(ctx context.Context) (int, string) { + // 模拟登录失败 + err := ErrLoginFailed.New(ctx) + return err.Code(), err.Error() + } + + codeZH, msgZH := handleLogin(zhCtx) + codeEN, msgEN := handleLogin(enCtx) + + fmt.Printf("中文响应: code=%d, msg=%s\n", codeZH, msgZH) + fmt.Printf("英文响应: code=%d, msg=%s\n", codeEN, msgEN) + + // 模拟参数校验失败 + handleParam := func(ctx context.Context, field string) (int, string) { + err := ErrParamInvalid.Newf(ctx, map[string]string{"field": field}) + return errorx.CodeFrom(err), err.Error() + } + fmt.Println() + code, msg := handleParam(zhCtx, "username") + fmt.Printf("参数校验(中文): code=%d, msg=%s\n", code, msg) + fmt.Println() +} + +// ====================================================================== +// 示例 5:中间件 — 统一错误处理 +// ====================================================================== +func exampleMiddleware() { + fmt.Println("=== 示例5:中间件模式 ===") + + // writeJSON 模拟写入 HTTP JSON 响应 + writeJSON := func(err error) { + code := errorx.CodeFrom(err) + msg := err.Error() + if code == -1 { + code = 500 + msg = "未知错误" + } + fmt.Printf("HTTP 响应: {\"code\":%d, \"msg\":\"%s\"}\n", code, msg) + } + + // 场景1:业务错误 + ctx := context.Background() + writeJSON(ErrTokenExpired.New(ctx)) + + // 场景2:带占位符的业务错误 + writeJSON(ErrUserNotFound.Newf(ctx, map[string]string{"name": "test_user"})) + + // 场景3:非 errorx 错误也能兜底 + writeJSON(http.ErrServerClosed) + + fmt.Println() +} + +// ====================================================================== +// 示例 6:error 链与 errors.Is / errors.As 兼容 +// ====================================================================== +func exampleErrorChain() { + fmt.Println("=== 示例6:与标准 errors 包兼容 ===") + + ctx := context.Background() + baseErr := ErrInternal.New(ctx) + wrappedErr := fmt.Errorf("处理订单失败: %w", baseErr) + + // errors.Is / errors.As 仍然可用 + e, ok := errorx.As(wrappedErr) + if ok { + fmt.Println("从包装后的 error 中提取成功:") + fmt.Println(" Key:", e.Key()) + fmt.Println(" Code:", e.Code()) + fmt.Println(" 消息:", e.Error()) + } + fmt.Println() +} + +// ====================================================================== +// 示例 7:动态创建错误码(未预定义的场景) +// ====================================================================== +func exampleDynamic() { + fmt.Println("=== 示例7:动态创建错误码 ===") + + // 运行时动态注册错误码 + dbError := errorx.NewCode("db_connection_failed", 503, "数据库连接失败: #detail#") + + ctx := context.Background() + err := dbError.Newf(ctx, map[string]string{"detail": "timeout after 30s"}) + + fmt.Println("Key:", errorx.KeyFrom(err)) + fmt.Println("Code:", errorx.CodeFrom(err)) + fmt.Println("消息:", err.Error()) + fmt.Println() +} + +// ---- main ------------------------------------------------------------ + +func main() { + // 初始化 langx + langx.InitLangx( + langx.SetDefaultCode(0), + langx.SetDefaultLanguage("zh"), + ) + + exampleBasic() + examplePlaceholder() + exampleHelpers() + exampleHTTP() + exampleMiddleware() + exampleErrorChain() + exampleDynamic() +} diff --git a/example/imports/main.go b/example/imports/main.go index baf5893..28df5e1 100644 --- a/example/imports/main.go +++ b/example/imports/main.go @@ -1,9 +1,11 @@ package main import ( + "context" "embed" "fmt" + "github.com/yuninks/errorx" "github.com/yuninks/langx" ) @@ -11,44 +13,78 @@ import ( var assetsFs embed.FS func main() { + langx.InitLangx( + langx.SetDefaultCode(0), + langx.SetDefaultLanguage("zh"), + ) + + fmt.Println("=== 方式1:逐条追加(适合少量错误码) ===") regByAppend() + + fmt.Println() + + fmt.Println("=== 方式2:embed 导入 JSON 文件(适合中型项目) ===") + regByEmbed() + + fmt.Println() + + fmt.Println("=== 方式3:目录文件导入(适合部署时外部管理) ===") + regByDir() } -// 导入语言包 基于Append +// 方式1:逐条追加 — 适合少量错误码或动态注册场景。 func regByAppend() { - langx.AppendCode(map[string]int{ - "success": 200, - }) - langx.AppendTrans("zh-CN", map[string]string{ - "success": "成功", - }) + // 先用 langx 注册语言包 + langx.AppendCode(map[string]int{"success": 200}) + langx.AppendTrans("zh-CN", map[string]string{"success": "成功!"}) - code, msg := langx.GetTransFormat("zh-CN", "success", map[string]string{}) - fmt.Println(code, msg) + // 然后用 errorx.NewCode 创建错误码(会自动调用 langx.Append*) + loginErr := errorx.NewCode("login_err", 401, "用户名或密码错误") + langx.AppendTrans("en", map[string]string{"login_err": "Invalid username or password"}) + ctx := context.Background() + err := loginErr.New(ctx) + + fmt.Printf(" code=%d, msg=%s\n", err.Code(), err.Error()) } -// 导入语言包 基于Embed +// 方式2:embed 导入 — JSON 文件编译进二进制,适合中型项目。 func regByEmbed() { - err := langx.RegisterEmbed(assetsFs) - fmt.Println(err) + if err := langx.RegisterEmbed(assetsFs); err != nil { + fmt.Println("embed 导入失败:", err) + return + } - code, msg := langx.GetTransFormat("zh", "success", map[string]string{}) - fmt.Println(code, msg) - code, msg = langx.GetTransFormat("en", "error", map[string]string{ - "msg": "这是失败的原因", + // JSON 中已声明的 error/success,直接用 key 创建 errorx 实例 + ctx := context.Background() + + err1 := errorx.NewError(ctx, "error") + fmt.Printf(" error: code=%d, msg(zh)=%s\n", err1.Code(), err1.Error()) + + err2 := errorx.NewError(ctx, "success") + fmt.Printf(" success: code=%d, msg(zh)=%s\n", err2.Code(), err2.Error()) + + // 带占位符 + err3 := errorx.NewErrorf(ctx, "error", map[string]string{ + "msg": "数据库连接超时", }) - fmt.Println(code, msg) + fmt.Printf(" error+format(zh): %s\n", err3.Error()) + + // 切换到英文 + err4 := errorx.NewErrorf(ctx, "error", map[string]string{ + "msg": "database connection timeout", + }).WithLang("en") + fmt.Printf(" error+format(en): %s\n", err4.Error()) } -// 导入语言包 基于文件 +// 方式3:目录文件导入 — 语言文件放在外部目录,支持运行时热加载。 func regByDir() { langx.RegisterDir("./lang") - code, msg := langx.GetTransFormat("zh", "success", map[string]string{}) - fmt.Println(code, msg) - code, msg = langx.GetTransFormat("en", "error", map[string]string{ - "msg": "这是失败的原因", + ctx := context.Background() + + err := errorx.NewErrorf(ctx, "error", map[string]string{ + "msg": "磁盘空间不足", }) - fmt.Println(code, msg) + fmt.Printf(" code=%d, msg=%s\n", errorx.CodeFrom(err), err.Error()) } diff --git a/example/imports/readme.md b/example/imports/readme.md index d1093cf..5a50e35 100644 --- a/example/imports/readme.md +++ b/example/imports/readme.md @@ -1,7 +1,47 @@ -# 导入资源 +# 语言包导入 -# 通过embed导入 +errorx 支持三种方式导入多语言资源。 -# 通过文件导入 +## 方式1:逐条追加(Append) -# 追加导入 +适合错误码较少、运行时动态注册的场景。 + +```go +langx.AppendCode(map[string]int{"success": 200}) +langx.AppendTrans("zh", map[string]string{"success": "成功"}) +``` + +## 方式2:Embed 导入 + +适合中型项目,JSON 文件编译进可执行文件。 + +目录结构: + +``` +lang/ + code.json → {"success": 200, "error": 400} + zh.json → {"success": "成功", "error": "失败 #msg#"} + en.json → {"success": "Success", "error": "Error #msg#"} +``` + +Go 代码: + +```go +//go:embed lang +var assetsFs embed.FS + +langx.RegisterEmbed(assetsFs) +``` + +## 方式3:目录导入 + +适合部署时外部管理语言包,支持不重启更新。 + +```go +langx.RegisterDir("./lang") +``` + +## JSON 格式 + +- `code.json`:错误码映射 `key → int` +- `{lang}.json`:翻译映射 `key → 翻译文本`,支持 `#placeholder#` 占位符 diff --git a/interfaces.go b/interfaces.go index 7abc22c..a0c2386 100644 --- a/interfaces.go +++ b/interfaces.go @@ -2,95 +2,142 @@ package errorx import ( "context" + "errors" "github.com/yuninks/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) // 设置附加数据键值对 +// ErrorInfo 是对外暴露的错误信息接口,语义精简、不可变。 +type ErrorInfo interface { + error + Code() int + Key() string + Format() map[string]string } -type defaultError struct { +// langError 是不可变的运行时错误实例,并发安全。 +// 通过 With* 方法派生新实例,永远不会修改自身。 +type langError struct { ctx context.Context key string format map[string]string } -func (l *defaultError) Copy() ErrorInterface { - return &defaultError{ - ctx: l.ctx, - key: l.key, - format: l.format, +// ---- 实现 error 接口 ----------------------------------------------- + +func (e *langError) Error() string { + return langx.GetFormat(langx.GetCtxLang(e.ctx), e.key, e.format) +} + +// ---- 实现 ErrorInfo 接口 -------------------------------------------- + +func (e *langError) Code() int { return langx.GetCode(e.key) } + +func (e *langError) Key() string { return e.key } + +func (e *langError) Format() map[string]string { + if len(e.format) == 0 { + return nil } + return e.copyFormat() } -func (e *defaultError) SetFormat(format map[string]string) { - e.format = format +// ---- 不可变派生方法(返回新实例,不修改原值)------------------------ + +// WithCtx 派生一个携带新上下文的错误实例。 +func (e *langError) WithCtx(ctx context.Context) *langError { + return &langError{ctx: ctx, key: e.key, format: e.copyFormat()} } -func (l *defaultError) SetFormatKV(key, value string) { - if l.format == nil { - l.format = make(map[string]string) +// WithLang 派生一个指定语言的错误实例。 +func (e *langError) WithLang(lang string) *langError { + return &langError{ctx: langx.SetCtxLang(e.ctx, lang), key: e.key, format: e.copyFormat()} +} + +// WithKV 派生一个添加单个占位符键值对的错误实例。 +func (e *langError) WithKV(k, v string) *langError { + newFmt := e.copyFormat() + if newFmt == nil { + newFmt = make(map[string]string) } - l.format[key] = value + newFmt[k] = v + return &langError{ctx: e.ctx, key: e.key, format: newFmt} } -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) +// WithMap 派生一个批量添加占位符键值对的错误实例(新 map 完全替换)。 +func (e *langError) WithMap(m map[string]string) *langError { + newFmt := make(map[string]string, len(m)) + for k, v := range m { + newFmt[k] = v } - return e.format + return &langError{ctx: e.ctx, key: e.key, format: newFmt} } -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, +func (e *langError) copyFormat() map[string]string { + if len(e.format) == 0 { + return nil } + dst := make(map[string]string, len(e.format)) + for k, v := range e.format { + dst[k] = v + } + return dst } -func NewError(ctx context.Context, key string) error { - return &defaultError{ - ctx: ctx, - key: key, +func copyMapString(src map[string]string) map[string]string { + if len(src) == 0 { + return nil } + dst := make(map[string]string, len(src)) + for k, v := range src { + dst[k] = v + } + return dst } -func NewStruct(ctx context.Context, key string, format map[string]string) ErrorInterface { - return &defaultError{ - ctx: ctx, - key: key, - format: format, +// ---- 构造函数 -------------------------------------------------------- + +// NewError 创建一个携带 ctx 的运行时错误。 +func NewError(ctx context.Context, key string) *langError { + return &langError{ctx: ctx, key: key} +} + +// NewErrorf 创建一个携带占位符键值对的运行时错误。 +func NewErrorf(ctx context.Context, key string, kv map[string]string) *langError { + return &langError{ctx: ctx, key: key, format: copyMapString(kv)} +} + +// ---- 辅助函数,简化 error → ErrorInfo 提取 --------------------------- + +// As 从 error 链中提取 *langError,失败返回 false。 +func As(err error) (*langError, bool) { + var e *langError + ok := errors.As(err, &e) + return e, ok +} + +// CodeFrom 从 error 中获取错误码;若不是 langError 则返回 -1。 +func CodeFrom(err error) int { + if e, ok := As(err); ok { + return e.Code() } + return -1 +} + +// KeyFrom 从 error 中获取 Key;若不是 langError 则返回空串。 +func KeyFrom(err error) string { + if e, ok := As(err); ok { + return e.Key() + } + return "" +} + +// FormatFrom 从 error 中获取占位符键值对(深拷贝);若不是 langError 则返回 nil。 +func FormatFrom(err error) map[string]string { + if e, ok := As(err); ok { + return e.Format() + } + return nil }