Files
errorx/README.md
T
yun 21293673e1 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
2026-09-21 00:43:43 +08:00

9.1 KiB
Raw Permalink Blame History

errorx

把「业务错误码 + 多语言文案 + 占位符」封装成不可变的错误常量,并补齐 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

安装

go get github.com/yuninks/errorx

快速开始

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
}

更多可运行示例:

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() 返回副本,派生不会影响原值,也不需要加锁:

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 动态构造:

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 打错时不会静默变成成功:

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 服务里使用

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。

日志

log.Printf("%+v", err)
// 数据库繁忙
//   cause: dial tcp 127.0.0.1:3306: connect: connection refused
//   stack:
//   main.loadUser
//       /app/main.go:47

并发

错误值不可变、包级常量没有共享可变状态,可以放心在任意 goroutine 使用:

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/       可运行示例

测试

go test ./...
go test -race ./...   # Windows 需要先装 gcc