前言

快一个月没有更新技术文章了,这段时间投注了较多的时间学习字节的开源项目 Kitex/Hertz ,并维护一些简单的 issue ,有兴趣的同学也可以去了解:

https://www.cloudwego.io/

这段时间迟迟没有更新文章,一方面是接触到了很多大佬,反观自身技术深度远远不及,变得不敢轻易下笔;另一方面反思了一下自己之前的写作,确实也有一些功利的成分,有时为了更新而更新,打算纠正。

接触开源之后,我感受到了开源社区打磨一个项目的认真与严谨,后续也希望自己能以此为鉴,对开源、对写作都是如此。

扯远了,写作这篇文章的原因是我在写单元测试的时候,有时会涉及 errors.Iserrors.As 方法的调用,借此做一个总结。

error 的定义

首先需要明确 Go 语言中的错误是通过接口定义的,因此是一个引用类型。

type error interface {
  Error() string
}
// Go 提供了一个默认实现
type errorString struct {
s string
}

func (e *errorString) Error() string {
return e.s
}

那么如果我要创建一个 error 实例,可以选择下面的方式:

func main() {
  // 此时创建的两个 error 都是 errorString 结构类型的
  errA := errors.New("new error a")
  fmt.Println(errA)
  errB := fmt.Errorf("new error %s", "b")
  fmt.Println(errB)
}
/*
打印结果:
new error a
new error b
*/

wrapError 的定义

wrapError 是嵌套的 error ,也实现了 error 接口的 Error 方法,本质也是一个 error ,并声明了一个 Unwrap 方法用于拆包装。

type wrapError struct {
msg string
err error
}

func (e *wrapError) Error() string {
return e.msg
}

func (e *wrapError) Unwrap() error {
return e.err
}

通过 fmt.Errorf 方法配合 %w 占位符创建嵌套类型的 wrapError。

var BaseErr = errors.New("the underlying base error")

func main() {
err1 := fmt.Errorf("wrap base: %w", BaseErr)
fmt.Println(err1)
err2 := fmt.Errorf("wrap err1: %w", err1)
fmt.Println(err2)
}
/*
 打印结果:
 wrap base: the underlying base error
 wrap err1: wrap base: the underlying base error
*/

为什么 fmt.Errorf 用了占位符 %w 之后创建的就是 wrapError 类型,而用了 fmt.Errorf 但只是选择其他占位符如上述示例中的 %s 创建的就是 errorString 类型?

可以简单看一下 fmt.Errorf 方法的源码:

func Errorf(format string, a ...any) error {
  p := newPrinter()
  p.wrapErrs = true
  p.doPrintf(format, a)
  s := string(p.buf)
  var err error
  if p.wrappedErr == nil {
     err = errors.New(s)
  } else {
     err = &wrapError{s, p.wrappedErr}
  }
  p.free()
  return err
}

核心就是 p.doPrintf(format, a) 调用后,如果包含 %w 占位符则会先创建内层的 error ,赋值给 p.wrappedErr ,从而触发 wrapError 的创建逻辑。

你也可以进一步去看 p.doPrintf(format, a) 的实现印证这个流程。

errors.Is

判断被包装的error是否包含指定错误。

var BaseErr = errors.New("the underlying base error")

func main() {
  err1 := fmt.Errorf("wrap base: %w", BaseErr)
  err2 := fmt.Errorf("wrap err1: %w", err1)
  println(err2 == BaseErr) // false
  if !errors.Is(err2, BaseErr) {
     panic("err2 is not BaseErr")
  }
  println("err2 is BaseErr")
}
/*
打印结果:
false
err2 is BaseErr
*/

来看一下 errors.Is 方法的源码:

func Is(err, target error) bool {
  if target == nil {
     return err == target
  }

  isComparable := reflectlite.TypeOf(target).Comparable()
  for {
     if isComparable && err == target {
        return true
    }
     if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
        return true
    }
     if err = Unwrap(err); err == nil {
        return false
    }
  }
}

func Unwrap(err error) error {
u, ok := err.(interface {
Unwrap() error
})
if !ok {
return nil
}
return u.Unwrap()
}

如果这个 err 自己实现了 interface{ Is(error) bool } 接口,通过接口断言,可以调用 Is 方法判断 err 是否与 target 相等。

否则递归调用 Unwrap 方法拆包装,返回下一层的 error 去判断是否与 target 相等。

errors.As

提取指定类型的错误,判断包装的 error 链中,某一个 error 的类型是否与 target 相同,并提取第一个符合目标类型的错误的值,将其赋值给 target。

type TypicalErr struct {
  e string
}

func (t TypicalErr) Error() string {
  return t.e
}

func main() {
  err := TypicalErr{"typical error"}
  err1 := fmt.Errorf("wrap err: %w", err)
  err2 := fmt.Errorf("wrap err1: %w", err1)
  var e TypicalErr
  if !errors.As(err2, &e) {
     panic("TypicalErr is not on the chain of err2")
  }
  println("TypicalErr is on the chain of err2")
  println(err == e)
}
/*
打印结果:
TypicalErr is on the chain of err2
true
*/

来看一下 error.As 方法的源码:

func As(err error, target any) bool {
  if target == nil {
     panic("errors: target cannot be nil")
  }
  val := reflectlite.ValueOf(target)
  typ := val.Type()
  if typ.Kind() != reflectlite.Ptr || val.IsNil() {
     panic("errors: target must be a non-nil pointer")
  }
  targetType := typ.Elem()
  if targetType.Kind() != reflectlite.Interface && !targetType.Implements(errorType) {
     panic("errors: *target must be interface or implement error")
  }
  for err != nil {
     if reflectlite.TypeOf(err).AssignableTo(targetType) {
        val.Elem().Set(reflectlite.ValueOf(err))
        return true
    }
     if x, ok := err.(interface{ As(any) bool }); ok && x.As(target) {
        return true
    }
     err = Unwrap(err)
  }
  return false
}

源码 for 循环前的部分是用来约束 target 参数的类型,要求其是一个非空的指针类型。

此外要求 *target 是一个接口或者实现了 error 接口。

for 循环判断 err 是否可以赋值给 target 所属类型,如果可以则赋值返回 true。

如果 err 实现了自己的 As 方法,则调用其逻辑,否则也是走递归拆包的逻辑。

小结

后续将继续分享一些源码解读的文章,关于 Go 语言的学习,我也开源了一个 GitHub 仓库,正在更新中,你也可以从我往期的文章中看到一些说明。

Go 源码解读|如何用好 errors 库的 errors.Is() 与 errors.As() 方法的更多相关文章

  1. SDWebImage源码解读之SDWebImageCache(下)

    第六篇 前言 我们在SDWebImageCache(上)中了解了这个缓存类大概的功能是什么?那么接下来就要看看这些功能是如何实现的? 再次强调,不管是图片的缓存还是其他各种不同形式的缓存,在原理上都极 ...

  2. Spark学习之路 (十六)SparkCore的源码解读(二)spark-submit提交脚本

    一.概述 上一篇主要是介绍了spark启动的一些脚本,这篇主要分析一下Spark源码中提交任务脚本的处理逻辑,从spark-submit一步步深入进去看看任务提交的整体流程,首先看一下整体的流程概要图 ...

  3. Spark Streaming源码解读之流数据不断接收和全生命周期彻底研究和思考

    本节的主要内容: 一.数据接受架构和设计模式 二.接受数据的源码解读 Spark Streaming不断持续的接收数据,具有Receiver的Spark 应用程序的考虑. Receiver和Drive ...

  4. Vue 源码解读(8)—— 编译器 之 解析(上)

    特殊说明 由于文章篇幅限制,所以将 Vue 源码解读(8)-- 编译器 之 解析 拆成了上下两篇,所以在阅读本篇文章时请同时打开 Vue 源码解读(8)-- 编译器 之 解析(下)一起阅读. 前言 V ...

  5. Vue 源码解读(12)—— patch

    前言 前面我们说到,当组件更新时,实例化渲染 watcher 时传递的 updateComponent 方法会被执行: const updateComponent = () => { // 执行 ...

  6. SDWebImage源码解读之SDWebImageDownloaderOperation

    第七篇 前言 本篇文章主要讲解下载操作的相关知识,SDWebImageDownloaderOperation的主要任务是把一张图片从服务器下载到内存中.下载数据并不难,如何对下载这一系列的任务进行设计 ...

  7. SDWebImage源码解读 之 NSData+ImageContentType

    第一篇 前言 从今天开始,我将开启一段源码解读的旅途了.在这里先暂时不透露具体解读的源码到底是哪些?因为也可能随着解读的进行会更改计划.但能够肯定的是,这一系列之中肯定会有Swift版本的代码. 说说 ...

  8. SDWebImage源码解读 之 UIImage+GIF

    第二篇 前言 本篇是和GIF相关的一个UIImage的分类.主要提供了三个方法: + (UIImage *)sd_animatedGIFNamed:(NSString *)name ----- 根据名 ...

  9. SDWebImage源码解读 之 SDWebImageCompat

    第三篇 前言 本篇主要解读SDWebImage的配置文件.正如compat的定义,该配置文件主要是兼容Apple的其他设备.也许我们真实的开发平台只有一个,但考虑各个平台的兼容性,对于框架有着很重要的 ...

随机推荐

  1. Linux综合运用

    一. Linux用户和组及其权限管理 请根据以下项目要求,写出操作过程和命令并进行相应的验证测试操作. 项目要求: 某软件开发公司即将开始在Linux系统上进行项目的开发.要实现的环境是:公司有软件开 ...

  2. jsx/tsx使用cssModule和typescript-plugin-css-modules

    目录 1,前言 2,效果图 3,如何使用 3.1,安装 3.2,配置 4,示例 5,插件错误处理 5.1,错误触发原因 5.2,解决办法 1,前言 在vite/webpack搭建的项目中,不管是vue ...

  3. Linux学习系列--如何在Linux中进行文件的管理

    文件 在常见的Linux的文件系统中,经常使用能了解到的文件管理系统是分为多个文件夹进行管理的. 如何查看文件路径 pwd ,在文件目录中,会有一个点(.)代表的是当前目录,两个点(..)代表的是当前 ...

  4. css基础02

    熟练快捷键!方便,要多练!  css复合选择器 不会选孙子,有一个儿子和另一个儿子的孩子(也是孙子)同名了,但子选择器子选择儿子,同名的孙子不选.和后代选择器有一点不一样的. " ,&quo ...

  5. 当我们谈论算法我们在谈论什么:由疫情核酸检测想到的分治算法(Divide-and-Conquer)

    原文转载自「刘悦的技术博客」https://v3u.cn/a_id_159 北京的疫情一波未平一波又起,由此看来,战"疫"将是一场旷日持久的战争,绝不能掉以轻心.轻易言胜.病毒随时 ...

  6. Java学习 (七)基础篇 变量

    变量 变量顾名思义,就是可以变化的量 Java是一种强类型语言,每个变量都必须声明其类型 Java变量是程序中最基本的存储单位,其要素包括变量名.变量类型和作用域 type varName [=val ...

  7. WorkflowAsCode 来了,Apache DolphinScheduler 2.0.2 惊喜发布

    文章目录 前言 01 新功能 1 WorkflowAsCode 2 企业微信告警方式支持群聊消息推送 02 优化 1 简化元数据初始化流程 2 删除补数日期中的"+1"(天) 03 ...

  8. Servlet特性研究之异步模式

    Servlet只有同步模型是怎样的? 异步处理是Servlet3.0版本的重要功能之一,分析异步处理模型之前,先看看同步处理的过程是怎样的: 客户端发起HTTP请求一个动态Servlet API,请求 ...

  9. 对Jmeter-基础线程组的一点解释

    概述 线程组是一个测试计划的起点.测试计划中所有元件的运行都必须依托于线程组.每个线程组都会独立的运行测试计划,互不干扰 线程数 线程数在并发用户场景下表示用户数,比如100用户同时发起请求 线程数在 ...

  10. RabbitMQ协议-AMQP 0-9-1 (高级消息队列协议)

    工作模型 producer:生产者 Connection:TCP长连接,AMQP 0-9-1 连接通常是长期存在的.AMQP 0-9-1 是一个应用层协议,它使用 TCP 进行可靠传输.连接使用身份验 ...