【Go】理解和使用错误链 | 客服服务营销数智化洞察_晓观点
       

【Go】理解和使用错误链

本文聊一下Go – errors 包的错误链的使用场景、实现以及应用,也弄清楚了一些容易混淆的概念。

在 Go 官方博客中有介绍 errors,尽管官方文章已经足够详细了。

本文主要是结合具体场景,有助于厘清容易混淆的概念。

errors 包的功能单纯,就是支持错误链——错误可以包裹(Wrap)另外的错误的能力

以 Go 1.23.3 的源码为例,除去 test 相关有三个简单文件,只有 3 个文件,定义了 5 个关键函数,共 300 行左右的代码(含注释)。

func As(err error, target any) bool
func Is(err, target error) bool
func Join(errs ...error) error
func New(text string) error 
func Unwrap(err error) error

1. error 和 errors 的区分

error 大家都很熟悉,它是 Go 语言的一个内置的接口定义,只要实现了 Error() string 它就是个 error

// The error built-in interface type is the conventional interface for// representing an error condition, with the nil value representing no error.
type error interface {
  Error() string
}

errors 是 Go 的标准库里的一个,主要是实现了错误操作相关的几个函数

Package errors implements functions to manipulate errors.

errors.New() 函数就可以创建一个 error。另外,fmt.Errorf 可以包装一个 error。

2. 错误链

2.1 什么是错误链

如果一个 error 实现了 Unwrap() 方法,返回一个底层错误,而这个底层错误可能也实现了 UnWrap() 方法,这样通过不断的 Unwrap() 就会形成一个错误序列——称作「错误链」

在 Go 说明errors的文档中有这么一句:

The most significant of these is a convention rather than a change: an error which contains another may implement an Unwrap method returning the underlying error. If e1.Unwrap() returns e2, then we say that e1wrapse2, and that you can unwrape1 to get e2.

如何理解「a convention rather than a change」?

这里的 convention(约定) 指错误类型实现 Unwrap,而这里的 change(改变) 应该是指对语言本身或者标准库没有改变。

虽然我们不断的提到 wrap,但是实际上只有 Unwrap() 方法,而没有 Wrap() 方法。我们来简单描述一下几个函数:

  • errors.New(): 创建一个包含错误信息的 error
  • fmt.Errorf(): 创建并包装一个或者多个 error
  • errors.Is(err, fs.ErrExist): 检查 err错误链中,是否有错误fs.ErrExist
  • errors.As(err, &perr): 检查 err 是否能赋值给 &perr(必须是个指针)并实际赋值
  • Unwrap(err): 调用 errUnwrap() 方法,如果没实现则返回 nil,解包装返回 []error 的也会返回 nil
  • Join(errs...): 合并多个err

2.2 为什么需要错误链

我们来看看什么场景下需要使用错误链,这有助于的我们理解它存在的意义,我们在碰到一样的场景也可以使用。

简单理解,错误链用在需要添加上下文信息 + 需要识别特定错误类型 的复杂错误处理逻辑中。

生活中的例子:在线购物

假设你正在开发一个电商平台,用户下单购买商品的过程中会经历多个步骤,比如商品选择、库存检查、支付、订单生成等。在每个步骤中都可能发生不同的错误,而这些错误的信息需要被逐层传递,最终帮助用户和开发人员明确问题所在。

场景 :在线购物中的错误链

假设有一个用户想购买一件商品,但在购买过程中发生了多个错误。每个错误的类型可能不一样,而且每个步骤都需要给出更多的上下文信息,以帮助开发者了解具体的问题。

  1. 商品库存不足: 用户选择了一个商品并放入购物车,但在库存检查时发现商品库存不足,系统会返回错误:“商品库存不足”。
  2. 支付失败: 用户选择商品并确认订单后,进入支付环节。如果支付过程中出现问题,比如信用卡信息错误,系统会返回错误:“支付失败:信用卡信息无效”。
  3. 订单生成失败: 如果支付成功,但在生成订单的时候由于系统故障,导致订单没有生成,系统返回错误:“订单生成失败:系统错误”。

错误链的实现:

在这个场景中,错误链的作用就是将每个环节发生的错误传递到上层,并且每个错误都可以附加不同的上下文信息,让用户和开发人员了解问题的具体原因。

我们考虑一个开发中会用到的场景

你封装了一个函数,查询Redis,

如果 Key 不存在(得到 redis.Nil 错误),

此时你需要将 Key 带出函数,并且外部仍然可以使用 redis.Nil 做判断

  • 如果直接使用 errors.New() 可以带出 key 信息,但是错误将不再是 redis.Nil
  • 如果直接范围 redis.Nil 则无法带出 key 信息

聪明的你,会写出这样的代码:

// 从 Redis 中获取值的函数
func getValueFromRedis(ctx context.Context, client *redis.Client, key string) (string, error) {
  val, err := client.Get(ctx, key).Result()
  if err != nil {
    if errors.Is(err, redis.Nil) {
      // 使用 fmt.Errorf 包装 redis.Nil 错误并添加额外信息
      return "", fmt.Errorf("key '%s' not found in Redis: %w", key, err)
    }
    return "", err
  }
  return val, nil
}

// 外部函数调用 getValueFromRedis
func main() {
  // 创建 Redis 客户端
  client := redis.NewClient(&redis.Options{
    Addr: "localhost:6379",
  })

  ctx := context.Background()

  // 尝试获取一个不存在的键
  key := "nonexistent_key"
  value, err := getValueFromRedis(ctx, client, key)
  if err != nil {
    // 打印错误信息
    fmt.Println("Error:", err)

    // 检查错误是否是 redis.Nil
    if errors.Is(err, redis.Nil) {
      fmt.Println("The error is redis.Nil")
    }
  } else {
    fmt.Println("Value:", value)
  }
}

上边的代码会输出:

Error: key 'nonexistent_key' not found in Redis: redis: nil
The error is redis.Nil

我们这里用到了 fmt.Errorf 进行了错误包装,即带出了 key 信息,又可以通过 errors.Is() 判断包装的错误是否是特定错误。

2.3 自定义的包装

上边的例子中,fmt.Errorf 实际是创建了一个新的对象:

【Go】理解和使用错误链

如果有更复杂的需求,我们可以自己实现这样的封装。比如除了 err 和 msg 之外,我们需要携带其他信息,或者有实现其他方法等情况。

2.4 更复杂的包装

上边的 fmt.Errorf() 如果参数中只有一个错误,会调用返回 wrapError,他有一个 Unwrap() 方法会返回 error。

fmt.Errorf() 也是可以包装多个错误,返回的是 wrapErrors,他的 Unwrap() 方法会返回 []error

我们前边也提到了函数,可以使用 errors.Join() 也有类似的作用。

如果上边的 newErr 是这样定义的,通过 errors.Is() 方法判断 newErr 是否为 err1err2err3 都会返回 true

2.5 不是所有的错误都需要错误链

上边提到 需要添加上下文信息 + 需要识别特定错误类型 的场景需要错误链,如果不都需要的话,则没必要使用错误链。

开发中通常会使用一个简单的代码如下。

(聪明的你,请先假设FetchValue()DefaultValue() 假设已经实现。)

value, err := c.FetchValue(args...)
if err != nil {
  value, err = c.DefaultValue(args...)
}

当在自己实现的一个简单的 Redis key-value 的 FetchValue() 的时候,他想:

  • 返回 redis.Nil 错误的时候,使用 DefaultValue()
  • 返回其他 Redis错误时,跳过使用 DefaultValue()

显然,上边仅仅通过 err != nil 是不能满足这种需求的。我们定义一个特殊的 error 就可以解决问题。因为这个场景中,GetValue() 中只需判断他是否需要使用默认值,而不需要带出FetchValue 方法内部的其他错误信息:

var errUseDefaultValue = errors.New("use default value")
...
func (c *CachableConfig[T]) GetValue(args ...any) (T, error) {
...
   value, err := c.FetchValue(args...)
  // 这里直接使用 == 比较也没问题
   if err == errUseDefaultValue {
       value, err = c.DefaultValue(args...)
   }

性能敏感的场景:在极端性能敏感的场景中,错误链的遍历可能会带来一些开销。因为 errors.Is()errors.As() 中会用到反射。

3. 源码学习

3.1 errors.Is()

Is() 封装了会先判断一下空,然后调用内部函数 is() 进行判断:

【Go】理解和使用错误链

内部函数 is() 会先直接比较一下是否跟 target 相等,然后看 err 本身是否有实现 Is() 方法,若都不相等,则不断的拆包装并比较。

我们也看到,这里有判断 Unwrap() 是返回的单个 error 还是 []error。如果前者,会覆盖 err 继续循环检查;如果是后者,会遍历每个 err 调用 is() 进行判断。

【编程技巧】这里外边 一个大的 for{} 可以针对返回单个 error 的场景,使用循环替代递归调用 is(),在性能上可以得到优化。

【Go】理解和使用错误链

3.2 errors.As()

As() 函数和 Is() 有些类似,封装了一个 as() 的内部函数,但是它会对 target 参数有更多的校验。

【Go】理解和使用错误链

因为内部函数 as()is() 很相似,这里就不再做详细解析,其中反射相关的在下一小节介绍。

3.3 反射的一些用法

在 wrap.go 文件中,Is() / As() 等函数中,有使用到 internal/reflectlite这个包,它是 Go 语言标准库中的一个内部包。

【编程技巧】Go语言中,internal包有一些特殊的规则,他其中的子包,上级目录可以导入这些子包中的内容,但上级目录之外的无法导入。这种设计主要是为了让开发者能够更好地封装内部实现细节,避免这些细节被项目之外的代码所依赖,从而提高代码的可维护性。

internal/reflectlite 包的主要用途是提供一个轻量级的反射(reflection)实现。其设计目的是在某些情况下替代标准库中的 reflect 包,以减少依赖和提高性能。其中,一些基本的反射功能包括:

  • 获取变量类型信息: reflectlite.TypeOf(err)targetType.Kind()
  • 读取和设置变量的值操作: 读取值 reflectlite.ValueOf(target)、设置值 targetVal.Elem().Set(reflectlite.ValueOf(err)
  • 类型检查: 检查一个类型是否实现了某个接口 targetType.Implements(errorType)、是否可以赋值 reflectlite.TypeOf(err).AssignableTo(targetType)
  • 方法调用: 获取方法 Method()、调用方法 Call() ,errors 包中没有使用到

4. 总结

4.1 几个概念不再混淆

看完上边的内容,之前可能混淆的概念,我们就清清楚楚了。简单列一下,就当巩固一下啦。

A. errors.Unwrap() 和自定义错误的 Unwrap()

  • errors.Unwrap() 需要一个error 输入参数,调用其 Unwrap 返回结果
  • 自定义错误的 Unwrap() 用于包装错误,在 errors 的 Is/As/Unwrap 函数中被调用

B. errors.Newfmt.Errorf 的区别

  • erros.New() 函数会创建一个 error
  • fmt.Errorf() 会包装一个或多个

C. Is()As() 的区别

  • Is() 使用来判断一个错误是否包含在错误链中
  • As() 将一个错误转换为特定的错误类型

D. 变量和类型的命名区别

  • 错误类型通常以 Error 结尾,比如 encoding/json 中的 UnmarshalTypeError
  • 通常以 Err 开头,比如 redis.ErrNil
免费试用 更多热门智能应用                        
(0)
智能客服组-Leo智能客服组-Leo
上一篇 2024年12月22日
下一篇 2024年12月22日

相关推荐