errkind

package module
v0.1.4 Latest Latest
Warning

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

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

README

errkind

Go Reference CI

A business error modeling library for Go 1.24+ — the core is a domain model of errors (Kind / instance separation, zero deps). Protocol adapters (HTTP / gRPC / OTel) and logger integrations (zap / zerolog / logrus) live in the ext/ decorators and integration/* submodules, each opted into on demand.

简体中文 | 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.24.

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 (unary & streaming) ToStatus(err) / FromStatus(st) / UnaryServerInterceptor() / UnaryClientInterceptor() / StreamServerInterceptor() / StreamClientInterceptor()
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")

Tooling

errkind ships two CLIs to keep error codes consistent across teams.

cmd/errkindlint — static collision check

Define panics on duplicate (code, name) at process init. errkindlint brings that check forward to compile time by statically scanning errkind.Define(...) literal calls across the whole repo (and across multiple go.mod boundaries).

go run github.com/im-wmkong/errkind/cmd/errkindlint -exclude=examples/ .
  • Reports duplicate code, duplicate name, and empty name
  • Exit code is non-zero on findings, ready for CI gating
  • -exclude=glob skips files (e.g. independent demo programs that intentionally reuse codes)
  • Accepts Go-style ./... paths
cmd/errkind doc — error code documentation

Generates a stable error-code catalogue from source — useful for frontend copywriters, SRE alert configs, and client codegen.

go run github.com/im-wmkong/errkind/cmd/errkind doc -format=md  ./...
go run github.com/im-wmkong/errkind/cmd/errkind doc -format=json ./...
go run github.com/im-wmkong/errkind/cmd/errkind doc -format=md -o errors.md ./...

Markdown output (excerpt):

| Code  | Name             | Default Message | Source                |
|------:|------------------|-----------------|-----------------------|
| 10001 | `user_not_found` | 用户不存在       | user/errors.go:12     |
| 10002 | `invalid_argument` | 参数非法       | user/errors.go:18     |

Both tools share internal/scan (AST-only, no init execution required), so they work even on code that fails to compile or has heavy framework deps.

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.24, go test -bench=. -benchtime=2s:

Benchmark ns/op B/op allocs/op
New() no option 17 96 1
New(Message) 17 96 1
New(With×3) 75 320 4
Wrap(cause, With) 31 128 2
New() with stack capture 198 144 2
Kind.Is(err) 1.2 0 0
CodeOf(err) 41 8 1
AllAttrs(depth=3) 91 224 3
fmt.Sprintf("%v", err) 66 80 3
fmt.Sprintf("%+v", err) no stack 80 160 4
json.Marshal(err) 717 448 15

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

Development

The repo is a multi-module Go workspace (main module + each integration/* under its own go.mod). go test ./... does not cross module boundaries, so use the bundled script to mirror CI locally:

./scripts/test.sh             # vet / build / test for every module
./scripts/test.sh -race       # extra args are forwarded to `go test`
./scripts/test.sh --group     # GitHub Actions ::group:: markers (used by CI)

The same script is the single source of truth for the CI test job — anything green locally is green on CI for those steps.

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
cmd
errkind command
Command errkind 提供错误码工程相关子命令; 当前实现 doc 子命令用于生成错误码文档。
Command errkind 提供错误码工程相关子命令; 当前实现 doc 子命令用于生成错误码文档。
errkindlint command
Command errkindlint 静态扫描 errkind.Define 调用, 检查 (code, name) 冲突。
Command errkindlint 静态扫描 errkind.Define 调用, 检查 (code, name) 冲突。
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
internal
docgen
Package docgen 把扫描得到的 errkind.Define 列表渲染成错误码文档。
Package docgen 把扫描得到的 errkind.Define 列表渲染成错误码文档。
lint
Package lint 基于 internal/scan 的扫描结果, 检测 (code, name) 冲突与显然错误。
Package lint 基于 internal/scan 的扫描结果, 检测 (code, name) 冲突与显然错误。
scan
Package scan 提供静态扫描能力, 在源码层面找出对 errkind.Define 的调用, 用于 errkindlint (冲突检测) 和 errkind doc (文档生成) 共享同一份解析逻辑。
Package scan 提供静态扫描能力, 在源码层面找出对 errkind.Define 的调用, 用于 errkindlint (冲突检测) 和 errkind doc (文档生成) 共享同一份解析逻辑。

Jump to

Keyboard shortcuts

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