golang-samber-oops by samber/cc-skills-golang
npx skills add https://github.com/samber/cc-skills-golang --skill golang-samber-oops角色设定: 你是一位将错误视为结构化数据的 Go 工程师。每个错误都携带足够的上下文信息——领域、属性、追踪——以便值班工程师无需询问开发者即可诊断问题。
samber/oops 是 Go 标准错误处理的即插即用替代方案,它增加了结构化上下文、堆栈追踪、错误代码、公共消息和恐慌恢复。变量数据放在 .With() 属性中(而不是消息字符串里),因此 APM 工具(Datadog、Loki、Sentry)可以正确地对错误进行分组。与标准库方法(在日志记录点添加 slog 属性)不同,oops 属性会随错误一起在调用栈中传递。
标准的 Go 错误缺乏上下文——你看到的是 connection failed,但不知道是哪个用户触发的、运行了什么查询,或者完整的调用堆栈。samber/oops 提供了:
.With() 属性中,而不是消息字符串里,因此 APM 工具可以正确地对错误进行分组此技能介绍并非详尽无遗。更多信息请参阅库文档和代码示例。Context7 可以作为一个发现平台提供帮助。
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
所有 oops 错误都使用流畅的构建器模式:
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...) — 将恐慌转换为错误| 方法 | 用例 |
|---|---|
.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) | 添加表示工作/操作单元的跨度 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 开始构建 |
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")
}
// ...
}
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)
}
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
}
// ✓ 良好 — 如果 err 为 nil,Wrap 返回 nil
return oops.Wrapf(err, "operation failed")
// ✗ 不佳 — 不必要的 nil 检查
if err != nil {
return oops.Wrapf(err, "operation failed")
}
return nil
每个架构层都应该通过 Wrap/Wrapf 添加上下文——至少在每个包边界一次(不一定在每个函数调用处)。
// ✓ 良好 — 每一层都添加相关上下文
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")
}
为了 APM 聚合,错误消息必须是低基数的。将变量数据插入到消息中会破坏 Datadog、Loki、Sentry 中的分组。
// ✗ 不佳 — 高基数,破坏 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")
oops.Recover() 必须在 goroutine 边界内使用。将恐慌转换为结构化错误:
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)
})
}
samber/oops 错误实现了标准的 error 接口。访问额外信息:
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")
fmt.Printf("%+v\n", err) // 详细输出,包含堆栈追踪
bytes, _ := json.Marshal(err) // 用于日志记录的 JSON
slog.Error(err.Error(), slog.Any("error", err)) // slog 集成
通过 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")
}
关于断言、配置和更多日志记录器示例,请参阅 高级模式。
samber/cc-skills-golang@golang-error-handling 技能,了解通用错误处理模式samber/cc-skills-golang@golang-observability 技能,了解日志记录器集成和结构化日志记录每周安装量
96
代码仓库
GitHub 星标数
184
首次出现
3 天前
安全审计
安装于
opencode78
codex77
gemini-cli77
cursor77
kimi-cli76
github-copilot76
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.
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.
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:
.With() attributes, not the message string, so APM tools group errors properlyThis skill is not exhaustive. Please refer to library documentation and code examples for more informations. Context7 can help as a discoverability platform.
All oops errors use a fluent builder pattern:
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| Methods | Use 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") |
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")
}
// ...
}
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)
}
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
}
// ✓ 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
Each architectural layer SHOULD add context via Wrap/Wrapf — at least once per package boundary (not necessarily at every function call).
// ✓ 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")
}
Error messages MUST be low-cardinality for APM aggregation. Interpolating variable data into the message breaks grouping in Datadog, Loki, Sentry.
// ✗ 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")
oops.Recover() MUST be used in goroutine boundaries. Convert panics to structured errors:
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)
})
}
samber/oops errors implement the standard error interface. Access additional info:
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")
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
Carry error context through Go contexts:
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.
samber/cc-skills-golang@golang-error-handling skill for general error handling patternssamber/cc-skills-golang@golang-observability skill for logger integration and structured loggingWeekly Installs
96
Repository
GitHub Stars
184
First Seen
3 days ago
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode78
codex77
gemini-cli77
cursor77
kimi-cli76
github-copilot76
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
116,600 周安装
| 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 |