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






