关于 golang-samber-oops
介绍 samber/oops 的 Go 技能。该结构化错误处理库可为错误附加上下文、堆栈跟踪、错误代码和公开消息,并支持从 panic 恢复。
主要功能
- 01附加键值结构化上下文
- 02捕获堆栈跟踪和错误代码
- 03区分公开消息与技术消息
- 04包装、合并和恢复错误
- 05将错误序列化为 JSON
适用场景
- 01在服务层边界补充领域上下文
- 02用稳定消息对 APM 错误分组
- 03将 goroutine panic 转为错误
- 04在错误中附加请求与追踪数据
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ samber/ cc-skills-golang --skill golang-samber-oops安装后在 Claude Code 中输入/golang-samber-oops确认已加载。
附带文件evals/evals.jsonreferences/advanced.md
详细介绍
角色: 你是一名 Go 工程师,将错误视为结构化数据。每个错误都携带足够的上下文——领域、属性、追踪信息——让值班工程师无需询问开发者就能诊断问题。
samber/oops 结构化错误处理
samber/oops 是 Go 标准错误处理方式的即插即用替代方案,增加了结构化上下文、堆栈跟踪、错误代码、公开消息和 panic 恢复功能。可变数据放在 .With() 属性中(而不是消息字符串中),这样 APM 工具(Datadog、Loki、Sentry)就能正确归类错误。与标准库的做法(在日志记录处添加 slog 属性)不同,oops 属性会随错误一起经过调用栈。
为什么使用 samber/oops
标准 Go 错误缺少上下文——你看到的是 connection failed,却不知道是哪个用户触发的、正在运行什么查询,也看不到完整的调用栈。samber/oops 提供:
- 结构化上下文——为任意错误添加键值属性
- 堆栈跟踪——自动捕获调用栈
- 错误代码——机器可读的标识符
- 公开消息——与技术细节分开的、适合用户查看的消息
- 低基数消息——将可变数据放在
.With()属性中,而不是消息字符串中,这样 APM 工具就能正确归类错误
本技能并未涵盖所有内容——如需了解更多信息,请参阅库文档和代码示例:
- 如需 Go 包文档、符号、版本、导入者和已知漏洞,→ 参阅
samber/cc-skills-golang@golang-pkg-go-dev技能(godig);查询 Go 包信息时优先于 Context7。 - 如需在自己的代码中查找此库的用法(定义、调用位置、诊断信息),→ 参阅
samber/cc-skills-golang@golang-gopls技能(gopls)。 - 对于未收录在 pkg.go.dev 上的文档,仍可将 Context7 作为备用方案。
核心模式:错误构建器链
所有 oops 错误都使用流式构建器模式:
err := oops.
In("user-service"). // domain/feature
Tags("database", "postgres"). // categorization
Code("network_failure"). // machine-readable identifier
User("user-123", "email", "[email protected]"). // user context
With("query", query). // custom attributes
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 转换为错误
错误构建器方法
| 方法 | 使用场景 |
|---|---|
.With("key", value) | 添加自定义键值属性(支持惰性 func() any 值) |
.WithContext(ctx, "key1", "key2") | 将 Go 上下文中的值提取为属性(支持惰性值) |
.In("domain") | 设置功能/服务/领域 |
.Tags("auth", "sql") | 添加分类标签(使用 err.HasTag("tag") 查询) |
.Code("iam_authz_missing_permission") | 设置机器可读的错误标识符/slug |
.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 开始构建 |
常见场景
数据库/存储库层
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 处理程序层
func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) {
userID := getUserID(r)
err := h.service.CreateUser(r.Context(), userID)
if err != nil {
err = oops.
In("http-handler").
Tags("endpoint", "/users").
Request(r, false).
User(userID).
Wrapf(err, "create user failed")
http.Error(w, oops.GetPublic(err, "Internal server error"), http.StatusInternalServerError)
return
}
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
}
错误包装最佳实践
应该:直接包装,无需进行 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
DO:在每一层添加上下文
每个架构层都应通过 Wrap/Wrapf 添加上下文——至少在每个包边界添加一次(不必在每次函数调用时都添加)。
// ✓ 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")
}
DO:保持错误消息的低基数
错误消息必须保持低基数,以便 APM 聚合。将可变数据插入消息会破坏 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")
Panic 恢复
必须在 goroutine 边界使用 oops.Recover()。将 panic 转换为结构化错误:
func ProcessData(data string) (err error) {
return oops.
In("data-processor").
Code("panic_recovered").
Hint("Check input data format and dependencies").
With("input_data", data).
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())
}
// 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
上下文传播
通过 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技能