olekukonko/errors
GitHub: olekukonko/errors
一个功能完备的 Go 生产级错误处理库,提供零成本抽象的堆栈追踪、结构化上下文、重试机制与多错误聚合能力。
Stars: 28 | Forks: 3
# errors — 专为 Go 打造的生产级错误处理库
[](https://pkg.go.dev/github.com/olekukonko/errors)
[](https://goreportcard.com/report/github.com/olekukonko/errors)
[](LICENSE)
[](https://golang.org/dl/)
一个功能完备的 Go 错误处理库。完全兼容 `errors.Is`、`errors.As` 和 `errors.Unwrap`。通过对象池、混合上下文存储以及抗内联的堆栈捕获机制,针对高吞吐量系统进行了优化。
## 目录
- [安装说明](#installation)
- [包概览](#package-overview)
- [核心 — `errors`](#core--errors)
- [创建错误](#creating-errors)
- [堆栈追踪](#stack-traces)
- [上下文](#context)
- [包装与链式处理](#wrapping-and-chaining)
- [哨兵错误](#sentinel-errors)
- [类型断言 — Is / As](#type-assertions--is--as)
- [多重错误聚合](#multi-error-aggregation)
- [重试](#retry)
- [链式执行](#chain-execution)
- [Channel 工具与流处理](#channel-utilities-and-streaming)
- [HTTP 辅助工具](#http-helpers)
- [并发组](#concurrent-group)
- [检查](#inspect)
- [slog 集成](#slog-integration)
- [池管理](#pool-management)
- [管理 — `errmgr`](#management--errmgr)
- [性能](#performance)
- [迁移指南](#migration-guide)
- [常见问题](#faq)
## 安装说明
```
go get github.com/olekukonko/errors@latest
```
需要 Go 1.21 或更高版本。
## 包概览
| 包 | 用途 |
|---|---|
| `errors` | 核心错误类型、包装、上下文、堆栈追踪、重试、链式处理、多重错误、channel 工具 |
| `errmgr` | 参数化错误模板、发生次数监控、阈值告警 |
## 核心 — `errors`
### 创建错误
```
// Fast — no stack trace, 0 allocations with pooling
err := errors.New("connection failed")
// Formatted — full fmt verb support including %w
err := errors.Newf("user %s not found", "alice")
err := errors.Errorf("query failed: %w", cause) // alias of Newf
// With stack trace
err := errors.Trace("critical issue")
err := errors.Tracef("query %s failed: %w", query, cause)
// Named — useful for sentinel-style matching
err := errors.Named("AuthError")
// Standard library compatible
err := errors.Std("connection failed") // returns plain error
err := errors.Stdf("error %s", "detail") // formatted plain error
```
### 堆栈追踪
```
// Capture at creation
err := errors.Trace("critical issue")
// Add to an existing error
err = err.WithStack()
// Read frames
for _, frame := range err.Stack() {
fmt.Println(frame) // "main.go:42 main.main"
}
// Lightweight version (file:line only, no function names)
for _, frame := range err.FastStack() {
fmt.Println(frame)
}
```
堆栈捕获不受编译器内联的影响 —— 堆栈帧是从物理调用栈中收集,并通过切片运算进行修剪,而不是通过跳过计数。
### 上下文
```
err := errors.New("processing failed").
With("user_id", "123").
With("attempt", 3).
With("retryable", true)
// Read back
ctx := errors.Context(err) // map[user_id:123 attempt:3 retryable:true]
// Check for a key
if err.HasContextKey("user_id") { ... }
// Variadic bulk attach
err.With("k1", v1, "k2", v2)
// Semantic helpers
err.WithCode(500)
err.WithCategory("network")
err.WithTimeout()
err.WithRetryable()
```
前四个上下文项存储在固定大小的数组中(无需分配内存)。
超出四个的项将溢出到 map 中。
### 包装与链式处理
```
lowErr := errors.New("connection timeout").With("server", "db01")
bizErr := errors.New("failed to load user").Wrap(lowErr)
apiErr := errors.Wrapf(bizErr, "request failed: %w", bizErr)
// Traverse
for i, e := range errors.UnwrapAll(apiErr) {
fmt.Printf("%d. %s\n", i+1, e)
}
// 1. request failed: ...
// 2. failed to load user
// 3. connection timeout
```
### 哨兵错误
`Const` 会创建一个稳定、可进行指针比较的哨兵,非常适合作为包级变量。
```
var (
ErrNotFound = errors.Const("not_found", "resource not found")
ErrForbidden = errors.Const("forbidden", "access denied")
)
// Match anywhere in a chain
if errors.Is(err, ErrNotFound) { ... }
// Add call-site context without losing the sentinel
err := ErrNotFound.With("user 42 not found")
errors.Is(err, ErrNotFound) // true — sentinel is the cause
// JSON and slog work automatically
b, _ := json.Marshal(ErrNotFound) // {"error":"resource not found","code":"not_found"}
slog.Error("lookup failed", "err", ErrNotFound)
```
### 类型断言 — Is / As
```
// Is — checks identity or name match
err := errors.Named("AuthError")
wrapped := errors.Wrapf(err, "login failed")
errors.Is(wrapped, err) // true
// As — extract the first matching *Error from the chain
var target *errors.Error
if errors.As(wrapped, &target) {
fmt.Println(target.Name()) // "AuthError"
}
// Generic helpers (Go 1.18+)
if e, ok := errors.AsType[*MyError](err); ok { ... }
if errors.IsType[*MyError](err) { ... }
found, ok := errors.FindType(err, func(e *MyError) bool {
return e.Code() == 404
})
codes := errors.Map(err, func(e *MyError) int { return e.Code() })
errors.Filter[*MyError](err) // [] *MyError from chain
errors.FirstOfType[*MyError](err) // first *MyError
```
### 多重错误聚合
```
// Basic
m := errors.NewMultiError()
m.Add(errors.New("name required"))
m.Add(errors.New("email invalid"))
fmt.Println(m.Count()) // 2
// With limits and sampling
m := errors.NewMultiError(
errors.WithLimit(100),
errors.WithSampling(10), // 10% sample rate
)
// Custom formatter
m := errors.NewMultiError(
errors.WithFormatter(func(errs []error) string {
return fmt.Sprintf("%d errors", len(errs))
}),
)
// Inspect
m.First() // first error
m.Last() // last error
m.Errors() // []error snapshot
m.Has() // bool
m.Single() // nil | first error | *MultiError
// Filter
networkErrs := m.Filter(func(e error) bool {
return strings.Contains(e.Error(), "network")
})
// Merge two MultiErrors
m.Merge(other)
// Join is a convenience that collapses errors to *MultiError or nil
err := errors.Join(err1, err2, err3)
```
### 重试
```
retry := errors.NewRetry(
errors.WithMaxAttempts(5),
errors.WithDelay(200*time.Millisecond),
errors.WithMaxDelay(2*time.Second),
errors.WithJitter(true),
errors.WithBackoff(errors.ExponentialBackoff{}),
errors.WithRetryIf(errors.IsRetryable),
errors.WithOnRetry(func(attempt int, err error) {
log.Printf("attempt %d: %v", attempt, err)
}),
)
err := retry.Execute(func() error {
return callExternalService()
})
// Generic version — preserves return value
result, err := errors.ExecuteReply[string](retry, func() (string, error) {
return fetchData()
})
// Context-aware
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
retry2 := retry.Transform(errors.WithContext(ctx))
err = retry2.Execute(fn)
// Backoff strategies
errors.ConstantBackoff{}
errors.LinearBackoff{}
errors.ExponentialBackoff{}
```
### 链式执行
按顺序执行的步骤,支持对每个步骤进行单独重试、设置超时、打标签以及配置可选步骤。
```
chain := errors.NewChain(
errors.ChainWithTimeout(10*time.Second),
errors.ChainWithLogHandler(slog.Default().Handler()),
).
Step(validateInput).Tag("validation").
Step(verifyKYC).Tag("kyc").
Step(processPayment).Tag("billing").Code(402).
Retry(3, 100*time.Millisecond, errors.WithRetryIf(errors.IsRetryable)).
Step(sendNotification).Tag("notification").Optional()
if err := chain.Run(); err != nil {
errors.Inspect(err, os.Stderr)
}
// Run all steps, collect every error
if err := chain.RunAll(); err != nil {
errors.Inspect(err, os.Stderr)
}
```
`StepCtx` 会将链级别的 context(及其 deadline)传递给各个步骤,因此像 HTTP 或数据库查询这样的阻塞调用都会遵守链的超时限制:
```
chain.StepCtx(func(ctx context.Context) error {
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
_, err := http.DefaultClient.Do(req)
return err
})
```
### Channel 工具与流处理
#### `<-chan error` 工具
这些工具与标准 Go 语言中的 `(chan T, chan error)` 惯用法进行组合,而不是替代它。
```
// Drain — block until channel closes, collect into *MultiError
err := errors.Drain(errs)
// First — return first non-nil error; ctx for deadline only, caller owns cancel
err := errors.First(ctx, errs)
if err != nil {
cancel() // caller decides to stop siblings
}
// Collect — bounded sample; wraps ErrLimitReached when n is hit
err := errors.Collect(ctx, errs, 10)
if errors.Is(err, errors.ErrLimitReached) {
log.Warn("more than 10 errors — some dropped")
}
// Fan — merge multiple error channels; caller must drain or cancel to avoid leak
merged := errors.Fan(ctx, validateErrs, enrichErrs)
for err := range merged {
log.Println(err)
}
```
#### Stream — 并发项处理
```
// Process items concurrently, collect all errors
s := errors.NewStream(ctx, urls, func(url string) error {
return fetch(url)
}, 8) // 8 workers; omit for len(items) workers
// Option A — block until done
if err := s.Wait(); err != nil {
errors.Inspect(err, os.Stderr)
}
// Option B — process errors as they arrive
s.Each(func(err error) {
log.Println(err)
})
// Stop early (drains channel to avoid goroutine leak)
s.Stop()
```
`Wait` 和 `Each` 是互斥的。对其中任何一个的第二次调用都会立即触发 panic。
### HTTP 辅助工具
```
// Resolve HTTP status from an *Error's code
status := errors.HTTPStatusCode(err, http.StatusInternalServerError)
// Write HTTP error response
errors.HTTPError(w, err) // plain text, status from err.Code()
// With options
errors.HTTPError(w, err,
errors.WithFallbackCode(http.StatusBadGateway),
errors.WithBody(false), // header only
errors.WithBodyFunc(func(e error) string {
return fmt.Sprintf(`{"error":%q}`, e.Error())
}),
)
```
### 并发组
`Group` 会收集并发 goroutine 产生的所有错误 —— 这与遇到第一个错误就会停止的 `errgroup` 不同。
```
g := errors.NewGroup()
g.Go(func() error { return validateUser(id) })
g.Go(func() error { return validatePerms(id) })
if err := g.Wait(); err != nil {
// err is *MultiError containing every failure
errors.Inspect(err, os.Stderr)
}
// Context-aware
g := errors.NewGroup(
errors.GroupWithContext(ctx, true), // cancelOnFirst=true
errors.GroupWithLimit(50),
)
g.GoCtx(func(ctx context.Context) error {
return longRunningCheck(ctx)
})
_ = g.Wait()
```
### 检查
```
// Default — writes to os.Stderr
errors.Inspect(err)
// Targeted output
var buf bytes.Buffer
errors.Inspect(err, &buf)
// Multiple destinations
errors.Inspect(err, os.Stderr, logFile)
// Options
errors.Inspect(err, os.Stderr,
errors.WithStackFrames(5),
errors.WithMaxDepth(20),
)
// *Error-specific convenience
errors.InspectError(err, os.Stderr)
```
`Inspect` 可以处理 `*Error`、`*MultiError` 以及任何标准库错误。它会将值写入提供的 `io.Writer`(通过 `io.MultiWriter` 合并),并且绝对不会触碰 stdout。
### slog 集成
`*Error` 和 `*Sentinel` 都实现了 `slog.LogValuer`:
```
slog.Error("request failed", "err", err)
// produces structured group: err.message, err.name, err.code, err.category, err.context, err.cause
slog.Error("lookup failed", "err", errors.ErrNotFound)
// produces: err.error="resource not found", err.code="not_found"
```
### 池管理
```
// Pre-warm (called automatically at init with 100 instances)
errors.WarmPool(1000)
errors.WarmStackPool(500)
// Tune global config
errors.Configure(errors.Config{
StackDepth: 32,
ContextSize: 4,
DisablePooling: false,
FilterInternal: true,
AutoFree: false, // opt-in GC-based pool return
})
// Explicit pool return (preferred)
err := errors.New("temp")
defer err.Free()
// Copy without affecting original
copied := err.Copy().With("extra", "data")
// Transform (non-destructive)
enriched := errors.Transform(err, func(e *errors.Error) {
e.WithCode(500).With("env", "prod").WithStack()
})
```
## 管理 — `errmgr`
### 参数化错误模板
```
// Define a reusable template
var ErrDBQuery = errmgr.Define("DBQuery", "database query failed: %s")
// Instantiate with arguments
err := ErrDBQuery("SELECT timed out")
fmt.Println(err) // "database query failed: SELECT timed out"
fmt.Println(err.Category()) // "database"
```
### 预定义错误
```
err := errmgr.ErrNotFound
fmt.Println(err.Code()) // 404
err := errmgr.ErrDBQuery("SELECT failed")
```
### 阈值监控
```
netErr := errmgr.Define("NetError", "network issue: %s")
monitor := errmgr.NewMonitor("NetError")
errmgr.SetThreshold("NetError", 3)
defer monitor.Close()
go func() {
for alert := range monitor.Alerts() {
fmt.Printf("alert: %s (count: %d)\n", alert, alert.Count())
}
}()
err := netErr("timeout")
err.Free()
```
关键设计决策:
- **Pool** — `New` 和 `Wrap` 会从 `sync.Pool` 复用 `*Error` 实例(12 ns/op,0 次分配)。
- **混合上下文** — 最多允许在固定大小的数组中存放 4 个键值对;溢出时再转入 map。避免了常见情况下的堆内存分配。
- **堆栈捕获** — `captureStack` 具有抗内联特性:它总是从 `runtime.Callers` 的第 1 帧开始,并通过数组切片进行修剪,因此编译器的内联决策永远不会破坏跳过计数。
- **保持 Pool 容量** — pool 缓冲区是通过原地修剪(`copy(buf, buf[trimmed:n])`)的方式处理的,而不是重新分配内存。这避免了在反复执行 `Free()` 循环时容量持续缩减的问题。
- **`MarshalJSON`** — 字节会在 pool 缓冲区返回之前被从中复制出来,从而消除了并发 JSON 序列化之间的竞态条件。
- **`With()`** — 互斥锁在入口处仅获取一次,消除了以往“先乐观读取,后加锁”路径中存在的 TOCTOU 竞态条件。
## 迁移指南
### 从标准库迁移
```
// Before
err := fmt.Errorf("user %s not found: %w", username, cause)
// After — same output, plus context, code, and chain traversal
err := errors.Newf("user %s not found: %w", username, cause).
With("username", username).
WithCode(404)
```
### 从 `pkg/errors` 迁移
```
// Before
err := pkgerrors.Wrap(cause, "operation failed")
// After
err := errors.New("operation failed").Wrap(cause).WithStack()
```
### 兼容标准库的 `errors.Is` / `errors.As`
```
// Fully compatible — no changes needed
if errors.Is(err, io.EOF) { ... }
var target *errors.Error
if errors.As(err, &target) {
fmt.Println(target.Name())
}
```
## 常见问题
**什么时候该用 `Const`,什么时候该用 `Named`?**
`Const` —— 包级别的哨兵错误,用于 `errors.Is` 匹配。每次调用都会返回相同的指针,因此可以使用指针相等性进行比较。`Named` —— 每次调用都会创建一个新的 `*Error` 实例;非常适合用于带有上下文的结构化错误,但不适用于 `==` 比较。
**什么时候该用 `Const`,什么时候该用 `errmgr.Define`?**
`errors.Const("not_found", "resource not found")` 会创建一个静态的哨兵错误。`errmgr.Define("DBQuery", "query failed: %s")` 会创建一个参数化的工厂方法 —— 你可以通过传入参数来调用它,从而每次都生成一个新的 `*Error`。
**什么时候应该调用 `Free()`?**
在错误生命周期很短,且你希望立即将其归还给 pool 的热点代码路径中。对于大多数应用程序代码,交给 GC 处理就足够了。如果在 `Config` 中启用了 `AutoFree`,GC 会自动回收错误 —— 但使用 `defer err.Free()` 会更可预测。
**为什么 `First` 不取消 context?**
`context.Context` 是不可变的 —— 只有 `context.WithCancel` 才能生成可取消的 context。`First` 接收 `ctx` 仅是为了提供 deadline 支持。其推荐模式是:调用 `First`,然后由你自己调用 `cancel()` 来停止其他并发项。
**为什么 `Stream` 上的 `Each` 和 `Wait` 在第二次调用时会引发 panic?**
对同一个 channel 消费两次会导致错误悄悄地在两个调用者之间被拆分。直接触发 panic 可以立刻暴露出这个 bug,而不是让它在生产环境中产生难以察觉的错误结果。
**如何调试深层的错误链?**
```
errors.Inspect(err, os.Stderr, errors.WithMaxDepth(30), errors.WithStackFrames(10))
```
**如何同时写入 stderr 和日志文件?**
```
errors.Inspect(err, os.Stderr, logFile) // io.MultiWriter internally
```
## 许可证
MIT — 详见 [LICENSE](LICENSE)。
标签:Go, Ruby工具, SOC Prime, 堆栈跟踪, 并发控制, 开发工具, 日志审计, 重试机制, 错误处理