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 }