golang错误处理增强插件库oops的使用

Golang 错误处理增强插件库 oops 的使用

Oops 是一个全面的 Go 错误处理库,提供带有丰富上下文信息的结构化错误管理。它设计为 Go 内置 error 的替代品,添加了强大的功能,如堆栈跟踪、源代码片段、结构化属性和开发者友好的调试提示。

🎯 主要特性

  • 🔧 无缝替代:无缝替换标准 Go 错误处理
  • 📊 丰富上下文:添加结构化属性、用户信息
  • 🐛 调试友好:开箱即用的堆栈跟踪和源代码片段
  • 🔗 错误链:用额外上下文包装和组合错误
  • 🛡️ 恐慌恢复:内置 panic 处理与错误转换
  • ✅ 断言:用于验证的单行断言辅助
  • ⚡ 性能:零依赖、轻量且快速
  • 📝 日志集成:适用于所有主流 Go 日志库
  • ✂️ 关注点分离:错误处理和日志记录是分开的工作
  • 🍳 易集成:无需大规模重构

🚀 安装

go get github.com/samber/oops

💡 快速开始

基础示例

import "github.com/samber/oops"

func main() {
    // 带上下文的简单错误
    err := oops.
        In("user-service").
        Tags("database", "postgres").
        Code("network_failure").
        User("user-123", "email", "foo@bar.com").
        With("path", "/hello/world").
        Errorf("failed to fetch user: %s", "connection timeout")
    
    // 错误包装
    if err != nil {
        return oops.
            Trace("req-123").
            With("product_id", "456").
            Wrapf(err, "user operation failed")
    }
}

错误构建器方法

方法 描述
.New(message string) error 创建并返回满足 error 接口的 oops.OopsError 对象
.Errorf(format string, args ...any) error 格式化错误并返回满足 error 接口的 oops.OopsError 对象
.Wrap(err error) error 将错误包装成满足 error 接口的 oops.OopsError 对象
.Wrapf(err error, format string, args ...any) error 包装错误并格式化错误消息
.Recover(cb func()) error 处理 panic 并返回满足 error 接口的 oops.OopsError 对象
.Recoverf(cb func(), format string, args ...any) error 处理 panic 并格式化错误消息
.Assert(condition bool) OopsErrorBuilder 如果条件为 false 则 panic,断言可以链式调用
.Assertf(condition bool, format string, args ...any) OopsErrorBuilder 如果条件为 false 则 panic 并格式化错误消息
.Join(err1 error, err2 error, ...) error 返回包装给定错误的错误

上下文构建器方法

方法 描述
.With(string, any) 提供键值对属性列表
.WithContext(context.Context, ...any) 提供上下文中声明的值列表
.Code(string) 设置描述错误的代码或 slug
.Public(string) 设置对终端用户安全的显示消息
.Time(time.Time) 设置错误时间(默认:time.Now()
.Since(time.Time) 设置错误持续时间
.Duration(time.Duration) 设置错误持续时间
.In(string) 设置功能类别或域
.Tags(...string) 添加多个标签,描述返回错误的功能
.Trace(string) 添加事务 ID、跟踪 ID、关联 ID…(默认:ULID)
.Span(string) 添加表示工作单元或操作的 span…(默认:ULID)
.Hint(string) 设置快速调试提示
.Owner(string) 设置负责处理此错误的同事/团队名称/电子邮件
.User(string, any...) 提供用户 ID 和键值链
.Tenant(string, any...) 提供租户 ID 和键值链
.Request(*http.Request, bool) 提供 http 请求
.Response(*http.Response, bool) 提供 http 响应

🧠 完整示例

错误包装示例

// 带错误包装
err0 := oops.
    In("repository").
    Tags("database", "sql").
    Wrapf(sql.Exec(query), "could not fetch user")  // 当 sql.Exec() 为 nil 时 Wrapf 返回 nil

// 带 panic 恢复
err1 := oops.
    In("repository").
    Tags("database", "sql").
    Recover(func () {
        panic("caramba!")
    })

// 带断言
err2 := oops.
    In("repository").
    Tags("database", "sql").
    Recover(func () {
        // ...
        oops.Assertf(time.Now().Weekday() == 1, "This code should run on Monday only.")
        // ...
    })

// oops.New
err3 := oops.
    In("repository").
    Tags("database", "sql").
    New("an error message")

// oops.Errorf
err4 := oops.
    In("repository").
    Tags("database", "sql").
    Errorf("an error message: %d", 42)

上下文丰富示例

// 带公共面向消息的简单错误
err0 := oops.
    Public("Could not fetch user.").
    Errorf("sql: bad connection")

// 带可选域的简单错误
err2 := oops.
    In("repository").
    Tags("database", "sql").
    Errorf("could not fetch user")

// 带自定义属性
ctx := context.WithContext(context.Background(), "a key", "value")
err3 := oops.
    With("driver", "postgresql").
    With("query", query).
    With("query.duration", queryDuration).
    With("lorem", func() string { return "ipsum" }).    // 延迟评估
    WithContext(ctx, "a key", "another key").
    Errorf("could not fetch user")

// 带 trace+span
err4 := oops.
    Trace(traceID).
    Span(spanID).
    Errorf("could not fetch user")

// 带提示和所有权,帮助开发者解决问题
err5 := oops.
    Hint("The user could have been removed. Please check deleted_at column.").
    Owner("Slack: #api-gateway").
    Errorf("could not fetch user")

Panic 处理示例

func mayPanic() {
    panic("permission denied")
}

func handlePanic() error {
    return oops.
        Code("iam_authz_missing_permission").
        In("authz").
        With("permission", "post.create").
        Trace("6710668a-2b2a-4de6-b8cf-3272a476a1c9").
        Hint("Runbook: https://doc.acme.org/doc/abcd.md").
        Recoverf(func() {
            // ...
            mayPanic()
            // ...
        }, "unexpected error %d", 42)
}

断言示例

func mayPanic() {
    x := 42

    oops.
        Trace("6710668a-2b2a-4de6-b8cf-3272a476a1c9").
        Hint("Runbook: https://doc.acme.org/doc/abcd.md").
        Assertf(time.Now().Weekday() == 1, "This code should run on Monday only.").
        With("x", x).
        Assertf(x == 42, "expected x to be equal to 42, but got %d", x)

    oops.Assert(re.Match(email))
    // ...
}

func handlePanic() error {
    return oops.
        Code("iam_authz_missing_permission").
        In("authz").
        Recover(func() {
            // ...
            mayPanic()
            // ...
        })
}

📫 日志集成

一些日志器可能需要自定义格式化程序来从 oops.OopsError 中提取属性。

可用日志器:

  • log
  • slog
  • logrus
  • zerolog

🥷 最佳实践

公共面向错误消息

err := oops.
    Public("Could not fetch user.").
    Errorf("sql: bad connection")

userMessage := oops.GetPublic(err, "Unexpected error")

错误构建器重用

errorBuilder := oops.
    In("iam").
    Trace("77cb6664").
    With("hello", "world")

err := mayFail1()
if err != nil {
    return errorBuilder.Wrap(err)
}

err = mayFail2()
if err != nil {
    return errorBuilder.Wrap(err)
}

return errorBuilder.Wrap(mayFail3())

调用者/被调用者属性

func a() error {
    return b()
}

func b() error {
    return oops.
        In("iam").
        Trace("4ea76885-a371-46b0-8ce0-b72b277fa9af").
        With("hello", "world").
        Wrapf(c(), "something failed")
}

func c() error {
    return d()
}

func d() error {
    return oops.
        Code("iam_missing_permission").
        In("authz").
        Time(time.Now()).
        With("permission", "post.create").
        Hint("Runbook: https://doc.acme.org/doc/abcd.md").
        User("user-123", "firstname", "john", "lastname", "doe").
        Tenant("organization-123", "name", "Microsoft").
        Errorf("permission denied")
}

Oops 提供了强大的错误处理能力,可以帮助开发者创建更结构化、更易于调试的错误信息。通过丰富的上下文信息和堆栈跟踪,可以大大简化错误诊断过程。


更多关于golang错误处理增强插件库oops的使用的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于golang错误处理增强插件库oops的使用的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


Golang 错误处理增强插件库 oops 使用指南

oops 是一个轻量级的 Go 错误处理增强库,它可以帮助开发者更好地追踪、记录和处理错误。下面我将详细介绍 oops 的使用方法,并提供示例代码。

安装 oops

首先,使用 go get 安装 oops 库:

go get github.com/samber/oops

基本用法

1. 创建带有上下文的错误

package main

import (
	"fmt"
	"github.com/samber/oops"
)

func main() {
	err := oops.
		Code("user_not_found").
		In("auth").
		Time("2023-01-01T00:00:00Z").
		With("user_id", 1234).
		Errorf("user %d not found", 1234)

	fmt.Printf("Error: %v\n", err)
	fmt.Printf("Detailed error: %+v\n", err)
}

2. 包装现有错误

func getUser(id int) error {
	_, err := db.GetUser(id)
	if err != nil {
		return oops.
			Wrapf(err, "failed to fetch user with id %d", id).
			With("user_id", id).
			In("repository").
			Code("db_error")
	}
	return nil
}

高级特性

1. 错误层级追踪

func processOrder(orderID int) error {
	err := validateOrder(orderID)
	if err != nil {
		return oops.
			Wrapf(err, "failed to process order %d", orderID).
			With("order_id", orderID).
			In("service")
	}
	return nil
}

func validateOrder(orderID int) error {
	err := checkInventory(orderID)
	if err != nil {
		return oops.
			Wrapf(err, "order validation failed for order %d", orderID).
			In("validation")
	}
	return nil
}

func checkInventory(orderID int) error {
	return oops.
		Errorf("insufficient inventory for order %d", orderID).
		In("inventory")
}

2. 错误断言和提取

func handleError(err error) {
	if oopsErr, ok := oops.AsOops(err); ok {
		fmt.Println("Error code:", oopsErr.Code())
		fmt.Println("Error domain:", oopsErr.Domain())
		fmt.Println("Error timestamp:", oopsErr.Timestamp())
		fmt.Println("Error context:", oopsErr.Context())
	}
	
	// 获取堆栈信息
	stack := oops.Stack(err)
	fmt.Println("Stack trace:", stack)
}

3. 自定义错误格式

func customFormatter(err error) string {
	oopsErr, _ := oops.AsOops(err)
	return fmt.Sprintf(
		"[%s] %s (code: %s, domain: %s)",
		oopsErr.Timestamp(),
		oopsErr.Error(),
		oopsErr.Code(),
		oopsErr.Domain(),
	)
}

func main() {
	oops.SetFormatter(customFormatter)
	
	err := oops.
		Code("test_error").
		In("test").
		Errorf("this is a test error")
	
	fmt.Println(err) // 使用自定义格式输出
}

最佳实践

  1. 在服务边界添加上下文:在服务层、仓库层等边界处添加适当的上下文信息
func (s *UserService) GetUser(id int) (*User, error) {
	user, err := s.repo.FindByID(id)
	if err != nil {
		return nil, oops.
			Wrapf(err, "failed to get user in service layer").
			With("user_id", id).
			In("user_service").
			Code("service_error")
	}
	return user, nil
}
  1. 使用错误码进行分类:定义清晰的错误码体系
const (
	ErrUserNotFound  = "user_not_found"
	ErrDBConnection  = "db_connection_error"
	ErrInvalidInput = "invalid_input"
)

func validateInput(input string) error {
	if input == "" {
		return oops.
			Errorf("input cannot be empty").
			Code(ErrInvalidInput)
	}
	return nil
}
  1. 记录完整的错误链:在日志中记录完整的错误信息
func logError(err error) {
	logger.Error().
		Err(err).
		Str("stack", oops.Stack(err)).
		Msg("An error occurred")
}

oops 库通过提供丰富的上下文信息和堆栈跟踪,大大增强了 Go 原生的错误处理能力,使得错误调试和问题定位变得更加容易。

回到顶部