Files
Error-Code-System/errcode/error.go
T
weichenandCursor bd53e9ba79
ci / vet-and-test (push) Successful in 3s
ci / docker-checkout-hostgateway (push) Successful in 3s
Add a shared error-code catalog so failures can be traced by stable codes.
Services can resolve legacy aliases and expose the catalog over CLI and HTTP instead of ad-hoc error strings.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-29 13:43:12 +00:00

155 lines
3.7 KiB
Go

package errcode
import (
"encoding/json"
"errors"
"fmt"
)
// E 是携带稳定错误码的错误。可用 errors.Is / errors.As 识别,也可 JSON 跨进程传输。
// 跨进程反序列化后 Cause 只保留文本,不再持有原始 Unwrap 链。
type E struct {
Code Code `json:"code"`
Reason string `json:"reason,omitempty"`
Fields map[string]any `json:"fields,omitempty"`
cause error
}
// New 构造一个不包装原因的错误。kv 必须成对出现,键必须是非空字符串;
// 这是调用方编程错误,会 panic(与 log/slog 的字段约定相同)。
func New(code Code, reason string, kv ...any) *E {
return &E{Code: code, Reason: reason, Fields: fieldsFrom(kv)}
}
// Wrap 用错误码包装 err。err 为 nil 时返回 nil,以便直接 return。
func Wrap(err error, code Code, reason string, kv ...any) *E {
if err == nil {
return nil
}
return &E{Code: code, Reason: reason, Fields: fieldsFrom(kv), cause: err}
}
// Error 实现 error。格式为 code: reason: cause,空段省略。
func (e *E) Error() string {
if e == nil {
return ""
}
msg := string(e.Code)
if e.Reason != "" {
msg += ": " + e.Reason
}
if e.cause != nil {
msg += ": " + e.cause.Error()
}
return msg
}
// Unwrap 返回被包装的原因。
func (e *E) Unwrap() error {
if e == nil {
return nil
}
return e.cause
}
// Is 在目标也是 *E 且错误码相同时匹配,供 errors.Is 使用。
func (e *E) Is(target error) bool {
t, ok := target.(*E)
if !ok || t == nil || e == nil {
return false
}
return e.Code != "" && e.Code == t.Code
}
// ErrorCode 返回规范码字符串。日志系统用鸭子接口读取,无需 import 本包。
func (e *E) ErrorCode() string {
if e == nil {
return ""
}
return string(e.Code)
}
// ErrorFields 返回字段副本(含 reason)。日志系统用鸭子接口读取。
func (e *E) ErrorFields() map[string]any {
if e == nil {
return nil
}
out := make(map[string]any, len(e.Fields)+1)
for k, v := range e.Fields {
out[k] = v
}
if e.Reason != "" {
out["reason"] = e.Reason
}
return out
}
// From 沿 Unwrap 链提取最外层 *E。不存在时返回 nil。
func From(err error) *E {
var e *E
if errors.As(err, &e) {
return e
}
return nil
}
// MarshalJSON 序列化码、原因、字段与原因文本。
func (e *E) MarshalJSON() ([]byte, error) {
if e == nil {
return []byte("null"), nil
}
wire := struct {
Code Code `json:"code"`
Reason string `json:"reason,omitempty"`
Fields map[string]any `json:"fields,omitempty"`
Cause string `json:"cause,omitempty"`
}{
Code: e.Code,
Reason: e.Reason,
Fields: e.Fields,
}
if e.cause != nil {
wire.Cause = e.cause.Error()
}
return json.Marshal(wire)
}
// UnmarshalJSON 恢复错误。原因文本包装为普通 error,Unwrap 不再指向原对象。
func (e *E) UnmarshalJSON(data []byte) error {
var wire struct {
Code Code `json:"code"`
Reason string `json:"reason,omitempty"`
Fields map[string]any `json:"fields,omitempty"`
Cause string `json:"cause,omitempty"`
}
if err := json.Unmarshal(data, &wire); err != nil {
return err
}
e.Code = wire.Code
e.Reason = wire.Reason
e.Fields = wire.Fields
e.cause = nil
if wire.Cause != "" {
e.cause = errors.New(wire.Cause)
}
return nil
}
func fieldsFrom(kv []any) map[string]any {
if len(kv) == 0 {
return nil
}
if len(kv)%2 != 0 {
panic("errcode: odd number of key-value arguments")
}
fields := make(map[string]any, len(kv)/2)
for i := 0; i < len(kv); i += 2 {
key, ok := kv[i].(string)
if !ok || key == "" {
panic(fmt.Sprintf("errcode: field key must be a non-empty string, got %T", kv[i]))
}
fields[key] = kv[i+1]
}
return fields
}