golang-samber-oops

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese
Persona: You are a Go engineer who treats errors as structured data. Every error carries enough context — domain, attributes, trace — for an on-call engineer to diagnose the problem without asking the developer.
角色定位: 你是一名将错误视为结构化数据的Go工程师。每个错误都携带足够的上下文——领域、属性、跟踪信息——以便值班工程师无需询问开发者就能诊断问题。

samber/oops Structured Error Handling

samber/oops 结构化错误处理

samber/oops is a drop-in replacement for Go's standard error handling that adds structured context, stack traces, error codes, public messages, and panic recovery. Variable data goes in
.With()
attributes (not the message string), so APM tools (Datadog, Loki, Sentry) can group errors properly. Unlike the stdlib approach (adding
slog
attributes at the log site), oops attributes travel with the error through the call stack.
samber/oops 是Go标准库错误处理的替代方案,它添加了结构化上下文、堆栈跟踪、错误代码、公开消息以及panic恢复功能。可变数据存储在
.With()
属性中(而非消息字符串),因此APM工具(Datadog、Loki、Sentry)可以正确地对错误进行分组。与标准库的方式(在日志记录位置添加
slog
属性)不同,oops的属性会随错误在调用栈中传递。

Why use samber/oops

为什么使用samber/oops

Standard Go errors lack context — you see
connection failed
but not which user triggered it, what query was running, or the full call stack.
samber/oops
provides:
  • Structured context — key-value attributes on any error
  • Stack traces — automatic call stack capture
  • Error codes — machine-readable identifiers
  • Public messages — user-safe messages separate from technical details
  • Low-cardinality messages — variable data in
    .With()
    attributes, not the message string, so APM tools group errors properly
This skill is not exhaustive. Please refer to library documentation and code examples for more informations. Context7 can help as a discoverability platform.
标准Go错误缺少上下文——你只会看到
connection failed
,但不知道是哪个用户触发的、正在运行什么查询,或者完整的调用堆栈。
samber/oops
提供以下功能:
  • 结构化上下文 —— 为任意错误添加键值对属性
  • 堆栈跟踪 —— 自动捕获调用堆栈
  • 错误代码 —— 机器可读的标识符
  • 公开消息 —— 与技术细节分离的用户安全消息
  • 低基数消息 —— 可变数据存储在
    .With()
    属性中而非消息字符串,确保APM工具能正确分组错误
本技能内容并非详尽无遗。更多信息请参考库文档和代码示例。Context7可作为发现平台提供帮助。

Core pattern: Error builder chain

核心模式:错误构建器链式调用

All
oops
errors use a fluent builder pattern:
go
err := oops.
    In("user-service").           // domain/feature
    Tags("database", "postgres").  // categorization
    Code("network_failure").       // machine-readable identifier
    User("user-123", "email", "foo@bar.com").  // user context
    With("query", query).          // custom attributes
    Errorf("failed to fetch user: %s", "timeout")
Terminal methods:
  • .Errorf(format, args...)
    — create a new error
  • .Wrap(err)
    — wrap an existing error
  • .Wrapf(err, format, args...)
    — wrap with a message
  • .Join(err1, err2, ...)
    — combine multiple errors
  • .Recover(fn)
    /
    .Recoverf(fn, format, args...)
    — convert panic to error
所有
oops
错误都使用流畅的构建器模式:
go
err := oops.
    In("user-service").           // 领域/功能模块
    Tags("database", "postgres").  // 分类标签
    Code("network_failure").       // 机器可读标识符
    User("user-123", "email", "foo@bar.com").  // 用户上下文
    With("query", query).          // 自定义属性
    Errorf("failed to fetch user: %s", "timeout")
终端方法:
  • .Errorf(format, args...)
    —— 创建新错误
  • .Wrap(err)
    —— 包装现有错误
  • .Wrapf(err, format, args...)
    —— 带消息包装错误
  • .Join(err1, err2, ...)
    —— 合并多个错误
  • .Recover(fn)
    /
    .Recoverf(fn, format, args...)
    —— 将panic转换为错误

Error builder methods

错误构建器方法

MethodsUse case
.With("key", value)
Add custom key-value attribute (lazy
func() any
values supported)
.WithContext(ctx, "key1", "key2")
Extract values from Go context into attributes (lazy values supported)
.In("domain")
Set the feature/service/domain
.Tags("auth", "sql")
Add categorization tags (query with
err.HasTag("tag")
)
.Code("iam_authz_missing_permission")
Set machine-readable error identifier/slug
.Public("Could not fetch user.")
Set user-safe message (separate from technical details)
.Hint("Runbook: https://doc.acme.org/doc/abcd.md")
Add debugging hint for developers
.Owner("team/slack")
Identify responsible team/owner
.User(id, "k", "v")
Add user identifier and attributes
.Tenant(id, "k", "v")
Add tenant/organization context and attributes
.Trace(id)
Add trace / correlation ID (default: ULID)
.Span(id)
Add span ID representing a unit of work/operation (default: ULID)
.Time(t)
Override error timestamp (default:
time.Now()
)
.Since(t)
Set duration based on time since
t
(exposed via
err.Duration()
)
.Duration(d)
Set explicit error duration
.Request(req, includeBody)
Attach
*http.Request
(optionally including body)
.Response(res, includeBody)
Attach
*http.Response
(optionally including body)
oops.FromContext(ctx)
Start from an
OopsErrorBuilder
stored in a Go context
方法适用场景
.With("key", value)
添加自定义键值对属性(支持延迟加载的
func() any
类型值)
.WithContext(ctx, "key1", "key2")
从Go上下文提取值到属性中(支持延迟加载值)
.In("domain")
设置功能模块/服务/领域
.Tags("auth", "sql")
添加分类标签(可通过
err.HasTag("tag")
查询)
.Code("iam_authz_missing_permission")
设置机器可读的错误标识符/短代码
.Public("Could not fetch user.")
设置用户安全消息(与技术细节分离)
.Hint("Runbook: https://doc.acme.org/doc/abcd.md")
为开发者添加调试提示
.Owner("team/slack")
指定负责团队/所有者
.User(id, "k", "v")
添加用户标识符和属性
.Tenant(id, "k", "v")
添加租户/组织上下文和属性
.Trace(id)
添加跟踪/关联ID(默认:ULID)
.Span(id)
添加表示工作单元/操作的Span ID(默认:ULID)
.Time(t)
覆盖错误时间戳(默认:
time.Now()
.Since(t)
根据自
t
以来的时长设置持续时间(可通过
err.Duration()
获取)
.Duration(d)
设置明确的错误持续时间
.Request(req, includeBody)
附加
*http.Request
(可选择包含请求体)
.Response(res, includeBody)
附加
*http.Response
(可选择包含响应体)
oops.FromContext(ctx)
从Go上下文中存储的
OopsErrorBuilder
开始构建

Common scenarios

常见场景

Database/repository layer

数据库/仓库层

go
func (r *UserRepository) FetchUser(id string) (*User, error) {
    query := "SELECT * FROM users WHERE id = $1"
    row, err := r.db.Query(query, id)
    if err != nil {
        return nil, oops.
            In("user-repository").
            Tags("database", "postgres").
            With("query", query).
            With("user_id", id).
            Wrapf(err, "failed to fetch user from database")
    }
    // ...
}
go
func (r *UserRepository) FetchUser(id string) (*User, error) {
    query := "SELECT * FROM users WHERE id = $1"
    row, err := r.db.Query(query, id)
    if err != nil {
        return nil, oops.
            In("user-repository").
            Tags("database", "postgres").
            With("query", query).
            With("user_id", id).
            Wrapf(err, "failed to fetch user from database")
    }
    // ...
}

HTTP handler layer

HTTP处理器层

go
func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) {
    userID := getUserID(r)

    err := h.service.CreateUser(r.Context(), userID)
    if err != nil {
        return oops.
            In("http-handler").
            Tags("endpoint", "/users").
            Request(r, false).
            User(userID).
            Wrapf(err, "create user failed")
    }

    w.WriteHeader(http.StatusCreated)
}
go
func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) {
    userID := getUserID(r)

    err := h.service.CreateUser(r.Context(), userID)
    if err != nil {
        return oops.
            In("http-handler").
            Tags("endpoint", "/users").
            Request(r, false).
            User(userID).
            Wrapf(err, "create user failed")
    }

    w.WriteHeader(http.StatusCreated)
}

Service layer with reusable builder

带可复用构建器的服务层

go
func (s *UserService) CreateOrder(ctx context.Context, req CreateOrderRequest) error {
    builder := oops.
        In("order-service").
        Tags("orders", "checkout").
        Tenant(req.TenantID, "plan", req.Plan).
        User(req.UserID, "email", req.UserEmail)

    product, err := s.catalog.GetProduct(ctx, req.ProductID)
    if err != nil {
        return builder.
            With("product_id", req.ProductID).
            Wrapf(err, "product lookup failed")
    }

    if product.Stock < req.Quantity {
        return builder.
            Code("insufficient_stock").
            Public("Not enough items in stock.").
            With("requested", req.Quantity).
            With("available", product.Stock).
            Errorf("insufficient stock for product %s", req.ProductID)
    }

    return nil
}
go
func (s *UserService) CreateOrder(ctx context.Context, req CreateOrderRequest) error {
    builder := oops.
        In("order-service").
        Tags("orders", "checkout").
        Tenant(req.TenantID, "plan", req.Plan).
        User(req.UserID, "email", req.UserEmail)

    product, err := s.catalog.GetProduct(ctx, req.ProductID)
    if err != nil {
        return builder.
            With("product_id", req.ProductID).
            Wrapf(err, "product lookup failed")
    }

    if product.Stock < req.Quantity {
        return builder.
            Code("insufficient_stock").
            Public("Not enough items in stock.").
            With("requested", req.Quantity).
            With("available", product.Stock).
            Errorf("insufficient stock for product %s", req.ProductID)
    }

    return nil
}

Error wrapping best practices

错误包装最佳实践

DO: Wrap directly, no nil check needed

建议:直接包装,无需空值检查

go
// ✓ Good — Wrap returns nil if err is nil
return oops.Wrapf(err, "operation failed")

// ✗ Bad — unnecessary nil check
if err != nil {
    return oops.Wrapf(err, "operation failed")
}
return nil
go
// ✓ 推荐 —— 若err为空,Wrap返回空值
return oops.Wrapf(err, "operation failed")

// ✗ 不推荐 —— 不必要的空值检查
if err != nil {
    return oops.Wrapf(err, "operation failed")
}
return nil

DO: Add context at each layer

建议:在每个层级添加上下文

Each architectural layer SHOULD add context via Wrap/Wrapf — at least once per package boundary (not necessarily at every function call).
go
// ✓ Good — each layer adds relevant context
func Controller() error {
    return oops.In("controller").Trace(traceID).Wrapf(Service(), "user request failed")
}

func Service() error {
    return oops.In("service").With("op", "create_user").Wrapf(Repository(), "db operation failed")
}

func Repository() error {
    return oops.In("repository").Tags("database", "postgres").Errorf("connection timeout")
}
每个架构层都应通过Wrap/Wrapf添加上下文——至少在每个包边界添加一次(不一定每个函数调用都添加)。
go
// ✓ 推荐 —— 每个层级添加相关上下文
func Controller() error {
    return oops.In("controller").Trace(traceID).Wrapf(Service(), "user request failed")
}

func Service() error {
    return oops.In("service").With("op", "create_user").Wrapf(Repository(), "db operation failed")
}

func Repository() error {
    return oops.In("repository").Tags("database", "postgres").Errorf("connection timeout")
}

DO: Keep error messages low-cardinality

建议:保持错误消息低基数

Error messages MUST be low-cardinality for APM aggregation. Interpolating variable data into the message breaks grouping in Datadog, Loki, Sentry.
go
// ✗ Bad — high-cardinality, breaks APM grouping
oops.Errorf("failed to process user %s in tenant %s", userID, tenantID)

// ✓ Good — static message + structured attributes
oops.With("user_id", userID).With("tenant_id", tenantID).Errorf("failed to process user")
错误消息必须是低基数的,以便APM工具聚合。将可变数据插入消息中会破坏Datadog、Loki、Sentry中的分组功能。
go
// ✗ 不推荐 —— 高基数,破坏APM分组
oops.Errorf("failed to process user %s in tenant %s", userID, tenantID)

// ✓ 推荐 —— 静态消息+结构化属性
oops.With("user_id", userID).With("tenant_id", tenantID).Errorf("failed to process user")

Panic recovery

Panic恢复

oops.Recover()
MUST be used in goroutine boundaries. Convert panics to structured errors:
go
func ProcessData(data string) (err error) {
    return oops.
        In("data-processor").
        Code("panic_recovered").
        Hint("Check input data format and dependencies").
        With("panic_value", r).
        Recover(func() {
            riskyOperation(data)
        })
}
oops.Recover()
必须在goroutine边界使用。将panic转换为结构化错误:
go
func ProcessData(data string) (err error) {
    return oops.
        In("data-processor").
        Code("panic_recovered").
        Hint("Check input data format and dependencies").
        With("panic_value", r).
        Recover(func() {
            riskyOperation(data)
        })
}

Accessing error information

访问错误信息

samber/oops
errors implement the standard
error
interface. Access additional info:
go
if oopsErr, ok := err.(oops.OopsError); ok {
    fmt.Println("Code:", oopsErr.Code())
    fmt.Println("Domain:", oopsErr.Domain())
    fmt.Println("Tags:", oopsErr.Tags())
    fmt.Println("Context:", oopsErr.Context())
    fmt.Println("Stacktrace:", oopsErr.Stacktrace())
}

// Get public-facing message with fallback
publicMsg := oops.GetPublic(err, "Something went wrong")
samber/oops
错误实现了标准的
error
接口。可通过以下方式访问额外信息:
go
if oopsErr, ok := err.(oops.OopsError); ok {
    fmt.Println("Code:", oopsErr.Code())
    fmt.Println("Domain:", oopsErr.Domain())
    fmt.Println("Tags:", oopsErr.Tags())
    fmt.Println("Context:", oopsErr.Context())
    fmt.Println("Stacktrace:", oopsErr.Stacktrace())
}

// 获取面向用户的消息,带默认值
publicMsg := oops.GetPublic(err, "Something went wrong")

Output formats

输出格式

go
fmt.Printf("%+v\n", err)       // verbose with stack trace
bytes, _ := json.Marshal(err)  // JSON for logging
slog.Error(err.Error(), slog.Any("error", err))  // slog integration
go
fmt.Printf("%+v\n", err)       // 带堆栈跟踪的详细输出
bytes, _ := json.Marshal(err)  // 用于日志的JSON格式
slog.Error(err.Error(), slog.Any("error", err))  // slog集成

Context propagation

上下文传播

Carry error context through Go contexts:
go
func middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        builder := oops.
            In("http").
            Request(r, false).
            Trace(r.Header.Get("X-Trace-ID"))

        ctx := oops.WithBuilder(r.Context(), builder)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

func handler(ctx context.Context) error {
    return oops.FromContext(ctx).Tags("handler", "users").Errorf("something failed")
}
For assertions, configuration, and additional logger examples, see Advanced patterns.
通过Go上下文传递错误上下文:
go
func middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        builder := oops.
            In("http").
            Request(r, false).
            Trace(r.Header.Get("X-Trace-ID"))

        ctx := oops.WithBuilder(r.Context(), builder)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

func handler(ctx context.Context) error {
    return oops.FromContext(ctx).Tags("handler", "users").Errorf("something failed")
}
断言、配置以及更多日志集成示例,请参阅高级模式

References

参考资料

Cross-References

交叉参考

  • → See
    samber/cc-skills-golang@golang-error-handling
    skill for general error handling patterns
  • → See
    samber/cc-skills-golang@golang-observability
    skill for logger integration and structured logging
  • → 查看
    samber/cc-skills-golang@golang-error-handling
    技能,了解通用错误处理模式
  • → 查看
    samber/cc-skills-golang@golang-observability
    技能,了解日志集成和结构化日志记录