golang-samber-oops

golang-samber-oops

热门

在 Golang 中使用 samber/oops 进行结构化错误处理——错误构建器、堆栈跟踪、错误码、错误上下文、错误包装、错误属性、面向用户与开发者的消息、恐慌恢复以及日志集成。适用于正在使用或采用 samber/oops 的场景,或代码库已导入 github.com/samber/oops 的情况。

2261Star
150Fork
更新于 2026/6/6
SKILL.md
只读
名称
golang-samber-oops
描述

在 Golang 中使用 samber/oops 进行结构化错误处理——错误构建器、堆栈跟踪、错误码、错误上下文、错误包装、错误属性、面向用户与开发者的消息、恐慌恢复以及日志集成。适用于正在使用或采用 samber/oops 的场景,或代码库已导入 github.com/samber/oops 的情况。

角色: 你是一位将错误视为结构化数据的 Go 工程师。每个错误都携带足够的上下文——领域、属性、追踪——以便值班工程师无需询问开发者即可诊断问题。

samber/oops 结构化错误处理

samber/oops 是 Go 标准错误处理的即插即用替代方案,增加了结构化上下文、堆栈跟踪、错误码、公共消息和恐慌恢复。变量数据放入 .With() 属性(而非消息字符串),以便 APM 工具(Datadog、Loki、Sentry)正确分组错误。与标准库方法(在日志记录点添加 slog 属性)不同,oops 属性随错误在调用栈中传递。

为什么使用 samber/oops

标准 Go 错误缺乏上下文——你看到 connection failed,但不知道是哪个用户触发的、正在运行什么查询、或者完整的调用栈。samber/oops 提供:

  • 结构化上下文——任何错误上的键值属性
  • 堆栈跟踪——自动调用栈捕获
  • 错误码——机器可读的标识符
  • 公共消息——与技术细节分离的用户安全消息
  • 低基数消息——变量数据放在 .With() 属性中,而非消息字符串,以便 APM 工具正确分组错误

本技能并非详尽无遗。请参考库文档和代码示例以获取更多信息。Context7 可作为发现平台提供帮助。对于 Go 包文档、版本、符号和已知漏洞,→ 请参阅 samber/cc-skills-golang@golang-pkg-go-dev 技能。

核心模式:错误构建器链

所有 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 context 中提取值作为属性(支持惰性值)
.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 context 中的 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 检查

// ✓ 好——如果 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("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())
}

// 获取面向公众的消息,带后备
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 context 携带错误上下文:

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 技能,了解日志集成和结构化日志