golang-samber-oops
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePersona: 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 attributes (not the message string), so APM tools (Datadog, Loki, Sentry) can group errors properly. Unlike the stdlib approach (adding attributes at the log site), oops attributes travel with the error through the call stack.
.With()slogsamber/oops 是Go标准库错误处理的替代方案,它添加了结构化上下文、堆栈跟踪、错误代码、公开消息以及panic恢复功能。可变数据存储在属性中(而非消息字符串),因此APM工具(Datadog、Loki、Sentry)可以正确地对错误进行分组。与标准库的方式(在日志记录位置添加属性)不同,oops的属性会随错误在调用栈中传递。
.With()slogWhy use samber/oops
为什么使用samber/oops
Standard Go errors lack context — you see but not which user triggered it, what query was running, or the full call stack. provides:
connection failedsamber/oops- 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 attributes, not the message string, so APM tools group errors properly
.With()
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 failedsamber/oops- 结构化上下文 —— 为任意错误添加键值对属性
- 堆栈跟踪 —— 自动捕获调用堆栈
- 错误代码 —— 机器可读的标识符
- 公开消息 —— 与技术细节分离的用户安全消息
- 低基数消息 —— 可变数据存储在属性中而非消息字符串,确保APM工具能正确分组错误
.With()
本技能内容并非详尽无遗。更多信息请参考库文档和代码示例。Context7可作为发现平台提供帮助。
Core pattern: Error builder chain
核心模式:错误构建器链式调用
All errors use a fluent builder pattern:
oopsgo
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:
- — create a new error
.Errorf(format, args...) - — wrap an existing error
.Wrap(err) - — wrap with a message
.Wrapf(err, format, args...) - — combine multiple errors
.Join(err1, err2, ...) - /
.Recover(fn)— convert panic to error.Recoverf(fn, format, args...)
所有错误都使用流畅的构建器模式:
oopsgo
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)—— 将panic转换为错误.Recoverf(fn, format, args...)
Error builder methods
错误构建器方法
| Methods | Use case |
|---|---|
| Add custom key-value attribute (lazy |
| Extract values from Go context into attributes (lazy values supported) |
| Set the feature/service/domain |
| Add categorization tags (query with |
| Set machine-readable error identifier/slug |
| Set user-safe message (separate from technical details) |
| Add debugging hint for developers |
| Identify responsible team/owner |
| Add user identifier and attributes |
| Add tenant/organization context and attributes |
| Add trace / correlation ID (default: ULID) |
| Add span ID representing a unit of work/operation (default: ULID) |
| Override error timestamp (default: |
| Set duration based on time since |
| Set explicit error duration |
| Attach |
| Attach |
| Start from an |
| 方法 | 适用场景 |
|---|---|
| 添加自定义键值对属性(支持延迟加载的 |
| 从Go上下文提取值到属性中(支持延迟加载值) |
| 设置功能模块/服务/领域 |
| 添加分类标签(可通过 |
| 设置机器可读的错误标识符/短代码 |
| 设置用户安全消息(与技术细节分离) |
| 为开发者添加调试提示 |
| 指定负责团队/所有者 |
| 添加用户标识符和属性 |
| 添加租户/组织上下文和属性 |
| 添加跟踪/关联ID(默认:ULID) |
| 添加表示工作单元/操作的Span ID(默认:ULID) |
| 覆盖错误时间戳(默认: |
| 根据自 |
| 设置明确的错误持续时间 |
| 附加 |
| 附加 |
| 从Go上下文中存储的 |
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 nilgo
// ✓ 推荐 —— 若err为空,Wrap返回空值
return oops.Wrapf(err, "operation failed")
// ✗ 不推荐 —— 不必要的空值检查
if err != nil {
return oops.Wrapf(err, "operation failed")
}
return nilDO: 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()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()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/oopserrorgo
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/oopserrorgo
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 integrationgo
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 skill for general error handling patterns
samber/cc-skills-golang@golang-error-handling - → See skill for logger integration and structured logging
samber/cc-skills-golang@golang-observability
- → 查看技能,了解通用错误处理模式
samber/cc-skills-golang@golang-error-handling - → 查看技能,了解日志集成和结构化日志记录
samber/cc-skills-golang@golang-observability