errkind

package module
v0.1.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 9, 2026 License: MIT Imports: 10 Imported by: 1

README

errkind

Go Reference CI

A business error modeling library for Go 1.23+ — not an error-handling library, not a stack library, not a gRPC library, but a domain model for business errors.

简体中文 | English

Design Principle

Identity (Kind) is separated from Instance (Error).

Kind                  Error
 ├─ Code               ├─ Kind  (refers to the same identity)
 └─ Name               ├─ Message
                       ├─ Attrs
                       └─ Cause
  • Kind is the identity of an error — a (code, name) global singleton, declared once at process start with Define, immutable forever.
  • Error is a concrete instance — every New / Wrap produces a fresh object carrying message / attrs / cause.

The result: a clean domain model, a tiny API surface, and full compatibility with the standard error ecosystem (errors.Is / errors.As / errors.Unwrap).

Install

go get github.com/im-wmkong/errkind

Minimum Go version: 1.23.

Quick Start

package main

import (
    stderrors "errors"
    "fmt"
    "log/slog"
    "os"

    "github.com/im-wmkong/errkind"
    httpext "github.com/im-wmkong/errkind/ext/http"
    slogext "github.com/im-wmkong/errkind/ext/slog"
)

// 1. Identity: defined once, global singleton.
var UserNotFound = errkind.Define(
    10001,
    "user_not_found",
    errkind.DefaultMessage("user not found"),
)

func getUser(id int64) error {
    cause := stderrors.New("sql: no rows in result set")
    // 2. Instance: each call yields a new error.
    err := UserNotFound.Wrap(cause, errkind.With("uid", id))
    // 3. ext decorator: protocol fields don't pollute core.
    return httpext.Status(404)(err)
}

func main() {
    err := getUser(42)

    // Both standard errors.Is and Kind.Is work.
    fmt.Println("Is UserNotFound:", UserNotFound.Is(err))

    // Pull out structured fields.
    if c, ok := errkind.CodeOf(err); ok {
        fmt.Println("Code:", c)
    }
    if c, ok := httpext.StatusOf(err); ok {
        fmt.Println("HTTP:", c)
    }

    // slog gets full structured output.
    slog.New(slog.NewJSONHandler(os.Stdout, nil)).
        Error("request failed", slogext.Err(err))
}

Output:

{"level":"ERROR","msg":"request failed","err":{
    "code":10001,"name":"user_not_found","message":"user not found",
    "attrs":{"uid":42},"http_status":404,
    "cause":"sql: no rows in result set"}}

Core API

Define a Kind
var UserNotFound = errkind.Define(10001, "user_not_found",
    errkind.DefaultMessage("user not found"),
)

Define panics on duplicate code/name to enforce singletons.

Create an Error
UserNotFound.New(opts...)            // no cause
UserNotFound.Wrap(cause, opts...)    // wrap a cause; cause==nil returns nil

Available options:

Option Effect
Message(s) Override the default message
Messagef(fmt, args...) Formatted message
With(k, v) Append an attr (same key overwrites, order preserved)
Match and Extract
UserNotFound.Is(err)                // true / false
errors.Is(err, sql.ErrNoRows)       // standard library sees through cause

c, ok := errkind.CodeOf(err)         // (Code, bool) — preferred
n, ok := errkind.NameOf(err)         // (string, bool)
msg   := errkind.MessageOf(err)      // falls back to err.Error() for non-errkind errors
attrs := errkind.AttrsOf(err)        // outermost attrs (copy)
flat  := errkind.AllAttrs(err)       // flattened across the chain, outer wins
Registry (test isolation / multi-tenant)
r := errkind.NewRegistry()
K := r.Define(1, "x")

Package-level Define / Kinds / LookupCode / LookupName use the default registry.

Stack Trace

A process-level switch, off by default:

errkind.SetCaptureStack(true)        // typically in main; enable in dev as needed

if t, ok := err.(errkind.Tracer); ok {
    for _, f := range t.StackTrace() { ... }
}

Why no per-call WithStack() option: forgetting to add it is the common case; a process-level toggle is the right default.

Formatting and Serialization
fmt.Sprintf("%v",  err)   // short:  user_not_found(10001): user not found: <cause>
fmt.Sprintf("%+v", err)   // multi-line: error info + (if captured) stack trace
fmt.Sprintf("%q",  err)   // short, quoted

json.Marshal(err)
// {"code":10001,"name":"user_not_found","message":"user not found",
//  "attrs":{"uid":42},"cause":"sql: no rows in result set"}

JSON output only contains core fields, never protocol fields like HTTP / gRPC — those are handled by the ext layer (e.g. slogext.Err). Attrs are emitted in insertion order; an attr value that can't be serialized (e.g. chan) is downgraded to a string instead of failing the whole marshal.

Extensions

errkind organizes extensions into two layers:

  • ext/ — protocol decorators with zero external dependencies. Always available inside the main module. Use to attach status codes / telemetry names to the error chain.
  • integration/ — heavy integrations with third-party frameworks. Each lives in its own Go module, so the main module stays free of unrelated dependencies.
ext/* (zero-dep decorators, in main module)
Package Purpose API
ext/http HTTP status code + JSON renderer Status(404)(err) / StatusOf(err) / Render(w, err)
ext/grpc gRPC status code (no grpc dep) Code(5)(err) / CodeOf(err)
ext/otel Telemetry name (no OTel dep) Name("biz.x")(err) / NameOf(err)
ext/slog log/slog integration (stdlib only) Err(err) / Value(err)
integration/* (separate modules, each pulls its own deps)
Module Purpose API
integration/grpc gRPC *status.Status round-trip + interceptors ToStatus(err) / FromStatus(st) / UnaryServerInterceptor() / UnaryClientInterceptor()
integration/otel Write errkind fields onto OTel spans RecordError(span, err) / Attributes(err)
integration/zap go.uber.org/zap Err(err) / Object(key, err)
integration/zerolog rs/zerolog Err(err) / Field(key, err) / Dict(err)
integration/logrus sirupsen/logrus Fields(err) / FieldsWithPrefix(prefix, err)

Logger snippets:

// zap
logger.Error("request failed", zapext.Err(err))

// zerolog
logger.Error().Func(zerologext.Err(err)).Msg("request failed")

// logrus
logger.WithFields(logrusext.Fields(err)).Error("request failed")

Comparison with Other Libraries

errkind pkg/errors cockroachdb/errors stdlib
Business error code ✅ ❌ partial (string hint) ❌
Identity / Instance separation ✅ ❌ ❌ ❌
Registry / collision check ✅ ❌ ❌ ❌
Compatible with errors.Is/As ✅ ✅ ✅ ✅
Stack trace process toggle always always ❌
HTTP / gRPC integration decorator ❌ built-in ❌
Zero-dep core package ✅ ✅ ❌ (heavy) ✅

When to choose errkind: business error codes need to be sliced by frontend / clients / OTel dimensions, and you want a clean domain model with open extension points.

When not to: you only want to attach a stack or a wrapping message to an error — fmt.Errorf("%w", err) is enough.

Stability

Currently v0.x; the API may still change. v1.0 will be cut after ≥6 months of production validation.

Performance

Apple M-series, Go 1.23, go test -bench=. -benchtime=2s:

Benchmark ns/op B/op allocs/op
New() no option 18 96 1
New(Message) 20 96 1
New(With×3) 89 320 4
Wrap(cause, With) 37 128 2
New() with stack capture 190 144 2
Kind.Is(err) 1.3 0 0
CodeOf(err) 43 8 1
AllAttrs(depth=3) 96 224 3
fmt.Sprintf("%v", err) 70 80 3
fmt.Sprintf("%+v", err) no stack 87 160 4
json.Marshal(err) 817 448 15

Run on your own box: go test -bench=. -benchmem ./...

Known Behavior Notes

  • Wrap(nil, ...) returns nil, matching fmt.Errorf("%w", nil).
  • errors.Is(err, kind) is not supported (*Kind does not implement error); use kind.Is(err) or errkind.CodeOf(err).
  • Define panics on duplicates; either code or name colliding, or an empty name, is rejected.
  • AttrsOf returns a copy — mutations don't affect the original error; same goes for AllAttrs.

License

MIT, see LICENSE.

Documentation

Overview

Package errkind 是业务错误建模库。

设计原则: Identity (Kind) 与 Instance (Error) 分离。

var UserNotFound = errkind.Define(10001, "user_not_found")

return UserNotFound.Wrap(cause,
    errkind.Message("用户不存在"),
    errkind.With("uid", uid),
)

core 不感知 HTTP / gRPC / OTel / slog 等任何外部协议; 这些扩展位于 ext/* 子包, 通过装饰器 (Decorator) 组合到错误链上, 由 errors.As 自然发现, 不依赖任何 core 内部"槽位"。

文件分布:

  • errkind.go 包文档 + 共享小类型 (Code / Attr)
  • kind.go Kind 身份对象
  • error.go kerr 实例, 含 Format / MarshalJSON
  • option.go Option 与内置 Message / With
  • registry.go Registry + KindOption + 包级默认 Registry
  • extract.go 从 error 链中提取信息的 helper
  • stack.go 调用栈 (进程级开关)

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func MessageOf

func MessageOf(err error) string

MessageOf 返回错误的 Message; 没有 errkind 错误时返回 err.Error()。

func NameOf

func NameOf(err error) (string, bool)

NameOf 返回错误的 Kind name 与是否找到; 是 KindOf 的 nil-safe 版本。

func SetCaptureStack

func SetCaptureStack(on bool)

SetCaptureStack 设置是否抓栈; 通常在 main 里调用一次。

Types

type Attr

type Attr struct {
	Key string
	Val any
}

Attr 是有序键值对; 使用切片而非 map, 保证遍历顺序稳定。

func AllAttrs

func AllAttrs(err error) []Attr

AllAttrs 沿错误链收集所有 errkind 错误的 attrs (扁平合并)。

同名 key 以"最外层"为准 (符合"内层是细节, 外层是上下文增强"的直觉); 99% 的日志场景需要这个扁平视图, 默认就给, 不让业务自己 Walk。

func AttrsOf

func AttrsOf(err error) []Attr

AttrsOf 返回最外层 errkind 错误的 attrs 拷贝; 没有则返回 nil。

返回的是浅拷贝, 调用方可以安全修改, 不会影响原错误。

type Code

type Code uint32

Code 是业务错误码的类型。

故意不用 int, 避免与 HTTP / gRPC 状态码、负数语义混淆; uint32 与 grpc/codes.Code 兼容, 与 4 字节序列化也对齐。

func CodeOf

func CodeOf(err error) (Code, bool)

CodeOf 返回错误的业务 Code 与是否找到; 是 KindOf 的 nil-safe 版本。

type Frame

type Frame struct {
	Function string
	File     string
	Line     int
}

Frame 是调用栈一帧。

func (Frame) String

func (f Frame) String() string

String 输出 "package.Func\n\tfile:line"。

type Kind

type Kind struct {
	// contains filtered or unexported fields
}

Kind 是错误的"身份"—— code + name 组成的全局单例, 永远不变。

Kind 由 Registry.Define 创建, 不能直接 new; 重复 (code, name) 会 panic。

func Define

func Define(code Code, name string, opts ...KindOption) *Kind

Define 在默认注册中心注册一个 Kind。

func KindOf

func KindOf(err error) *Kind

KindOf 从 err 链上提取第一个 errkind 错误的 Kind。

注意: 当 err 不是 errkind 错误时返回 nil。 推荐使用 CodeOf / NameOf 这两个 (T, bool) 风格的 helper, 避免空指针解引用:

if c, ok := errkind.CodeOf(err); ok && c == UserNotFound.Code() { ... }

或者显式 nil 判:

if k := errkind.KindOf(err); k != nil && k == UserNotFound { ... }

func Kinds

func Kinds() []*Kind

Kinds 返回默认注册中心的所有 Kind。

func LookupCode

func LookupCode(c Code) *Kind

LookupCode 默认注册中心查询。

func LookupName

func LookupName(n string) *Kind

LookupName 默认注册中心查询。

func (*Kind) Code

func (k *Kind) Code() Code

Code 返回业务错误码。

func (*Kind) DefaultMessage

func (k *Kind) DefaultMessage() string

DefaultMessage 返回默认消息, 可能为空。

func (*Kind) Is

func (k *Kind) Is(err error) bool

Is 判断 err 链上是否含有本 Kind 的实例 (按 Kind 指针相等)。

不依赖 code / name 字符串比较——避免不同 Registry 同 code 误判。

func (*Kind) Name

func (k *Kind) Name() string

Name 返回稳定的、可被日志/OTel/指标使用的标识 (推荐 snake_case)。

func (*Kind) New

func (k *Kind) New(opts ...Option) error

New 创建一个不带 cause 的新错误实例。

func (*Kind) Wrap

func (k *Kind) Wrap(cause error, opts ...Option) error

Wrap 包装一个 cause; cause == nil 时返回 nil, 与 fmt.Errorf("%w", nil) 一致。

type KindOption

type KindOption func(*Kind)

KindOption 用于 Define 时配置 Kind 的默认行为。

func DefaultMessage

func DefaultMessage(msg string) KindOption

DefaultMessage 给 Kind 设置默认消息, New / Wrap 未传 Message 时回退。

type Option

type Option func(*kerr)

Option 作用于 errkind 错误实例自身 (message / attrs)。

协议相关扩展 (HTTP / gRPC / ...) 不通过 Option, 而是由 ext 包独立装饰器实现, 这两个机制不混用——避免一个泛槽位变成什么都往里塞的字典。

func Message

func Message(msg string) Option

Message 设置消息; 多次调用以最后一次为准。

func Messagef

func Messagef(format string, args ...any) Option

Messagef 等价于 Message(fmt.Sprintf(format, args...))。

func With

func With(key string, val any) Option

With 追加一条 attr; 同名 key 覆盖, 保持原插入位置。

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry 持有一组 Kind, 保证 (code, name) 在自身范围内唯一。

通常使用包级 Define / Kinds / LookupCode / LookupName 即可; 测试或多租户场景可以 NewRegistry() 创建独立注册中心。

func NewRegistry

func NewRegistry() *Registry

NewRegistry 创建一个独立的注册中心。

func (*Registry) Define

func (r *Registry) Define(code Code, name string, opts ...KindOption) *Kind

Define 注册并返回一个新的 Kind; 重复 code/name 立即 panic。

func (*Registry) Kinds

func (r *Registry) Kinds() []*Kind

Kinds 返回所有已注册 Kind, 按注册顺序; 用于生成错误码文档。

func (*Registry) LookupCode

func (r *Registry) LookupCode(c Code) *Kind

LookupCode 按 code 查找; 不存在返回 nil。

func (*Registry) LookupName

func (r *Registry) LookupName(n string) *Kind

LookupName 按 name 查找; 不存在返回 nil。

type Tracer

type Tracer interface {
	StackTrace() []Frame
}

Tracer 表示能提供调用栈的错误。

Directories

Path Synopsis
examples
basic command
演示 errkind 的最小可运行用法。
演示 errkind 的最小可运行用法。
http command
演示 errkind 在 net/http 服务里如何统一渲染错误响应。
演示 errkind 在 net/http 服务里如何统一渲染错误响应。
ext
grpc
Package grpc 把 gRPC 状态码装饰到错误链上。
Package grpc 把 gRPC 状态码装饰到错误链上。
http
Package http 把 HTTP 状态码装饰到错误链上。
Package http 把 HTTP 状态码装饰到错误链上。
otel
Package otel 提供错误的 telemetry 命名约定 (用于 metrics / tracing 维度切分)。
Package otel 提供错误的 telemetry 命名约定 (用于 metrics / tracing 维度切分)。
slog
Package slog 把 errkind 错误结构化输出到 log/slog。
Package slog 把 errkind 错误结构化输出到 log/slog。
integration
grpc module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL