mcpField
G
golang-samber-oops

golang-samber-oops

github.com/samber开发3.7k38.6k 次安装5 天前更新
代码仓库

关于 golang-samber-oops

介绍 samber/oops 的 Go 技能。该结构化错误处理库可为错误附加上下文、堆栈跟踪、错误代码和公开消息,并支持从 panic 恢复。

主要功能

  1. 01附加键值结构化上下文
  2. 02捕获堆栈跟踪和错误代码
  3. 03区分公开消息与技术消息
  4. 04包装、合并和恢复错误
  5. 05将错误序列化为 JSON

适用场景

  1. 01在服务层边界补充领域上下文
  2. 02用稳定消息对 APM 错误分组
  3. 03将 goroutine panic 转为错误
  4. 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 技能

评论

支持 Markdown