Files
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

110 lines
3.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package main 演示在 HTTP 服务里使用 errorx:
// 业务层只管 return error,出口统一翻译成 {code, msg}。
//
// 运行:go run ./example/http
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"github.com/yuninks/errorx"
"github.com/yuninks/langx"
)
var (
ErrParamInvalid = errorx.New("param_invalid", 10002, "参数错误: #field#")
ErrUserNotFound = errorx.New("user_not_found", 10001, "用户不存在")
ErrDB = errorx.New("db_error", 50001, "数据库繁忙")
)
func init() {
langx.AppendTrans("en", map[string]string{
"param_invalid": "invalid parameter: #field#",
"user_not_found": "user not found",
"db_error": "database is busy",
})
}
// Response 是对外的统一响应体
type Response struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
// 业务层:只返回 error,完全不关心 HTTP 与语言
func loadUser(ctx context.Context, id string) (string, error) {
switch id {
case "":
return "", ErrParamInvalid.ErrKV("field", "id")
case "404":
return "", ErrUserNotFound.Err()
case "500":
return "", ErrDB.Wrap(errors.New("dial tcp 127.0.0.1:3306: connect: connection refused"))
default:
return "用户-" + id, nil
}
}
// 出口层:把 error 统一翻译成响应
func userHandler(w http.ResponseWriter, r *http.Request) {
ctx := withLang(r)
name, err := loadUser(ctx, r.URL.Query().Get("id"))
if err != nil {
writeJSON(w, toResponse(ctx, err))
return
}
writeJSON(w, Response{Code: errorx.Success.GetCode(), Msg: name})
}
// withLang 把请求头里的语言放进 ctx。
// langx 约定从 ctx 里读 "Accept-Language",也可以自己用 langx.SetCtxLang 指定。
func withLang(r *http.Request) context.Context {
return context.WithValue(r.Context(), "Accept-Language", r.Header.Get("Accept-Language"))
}
// toResponse 是所有 error 的唯一出口:errorx 错误取业务码,其它错误按 500 处理
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)}
}
func writeJSON(w http.ResponseWriter, resp Response) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(resp)
}
func main() {
fmt.Println("=== 同一个接口,不同入参与语言 ===")
cases := []struct{ id, lang string }{
{"", "zh"},
{"404", "zh"},
{"404", "en"},
{"500", "en"},
{"42", "zh"},
}
for _, c := range cases {
r := httptest.NewRequest(http.MethodGet, "/user?id="+c.id, nil)
r.Header.Set("Accept-Language", c.lang)
w := httptest.NewRecorder()
userHandler(w, r)
// 统一返回 HTTP 200,业务状态放在 code 字段(国内常见约定),
// 需要真实 HTTP 状态码时,可以在 toResponse 里按 code 映射。
fmt.Printf("GET /user?id=%-4s [%s] -> HTTP %d %s", c.id, c.lang, w.Code, w.Body.String())
}
fmt.Println()
fmt.Println("=== 日志里打印完整信息(带 cause 和调用栈)===")
r := httptest.NewRequest(http.MethodGet, "/user?id=500", nil)
_, err := loadUser(withLang(r), "500")
fmt.Printf("%+v", err)
fmt.Println()
}