olekukonko/errors

GitHub: olekukonko/errors

一个功能完备的 Go 生产级错误处理库,提供零成本抽象的堆栈追踪、结构化上下文、重试机制与多错误聚合能力。

Stars: 28 | Forks: 3

# errors — 专为 Go 打造的生产级错误处理库 [![Go Reference](https://pkg.go.dev/badge/github.com/olekukonko/errors.svg)](https://pkg.go.dev/github.com/olekukonko/errors) [![Go Report Card](https://goreportcard.com/badge/github.com/olekukonko/errors)](https://goreportcard.com/report/github.com/olekukonko/errors) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Go 1.21+](https://img.shields.io/badge/go-1.21+-blue.svg)](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, 堆栈跟踪, 并发控制, 开发工具, 日志审计, 重试机制, 错误处理