Golang 学习——error 和创建 error 源码解析

Golang 中 error 和创建 error 源码解析#

Golang 中的错误处理和 Java,Python 有很大不同,没有 try...catch 语句来处理错误。因此,Golang 中的错误处理是一个比较有争议的点,如何优雅正确的处理错误是值得去深究的。

今天先记录 error 是什么及如何创建 error,撸一撸源码。

一。初识 error#

1. 什么是 error#

error 错误指的是可能出现问题的地方出现了问题。比如打开一个文件时失败,这种情况在人们的意料之中 。

而异常指的是不应该出现问题的地方出现了问题。比如引用了空指针,这种情况在人们的意料之外。

可见,错误是业务过程的一部分,而异常不是 。

Golang 中的错误也是一种类型。错误用内置的 error 类型表示。就像其他类型,如 intfloat64 等。

错误值可以存储在变量中,也可以从函数中返回,等等。

2.error 源码#

src/builtin/builtin.go 文件下,定义了错误类型,源码如下:

// src/builtin/builtin.go

// 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
}

error 是一个接口类型,它包含一个 Error() 方法,返回值为 string。任何实现这个接口的类型都可以作为一个错误使用,Error 这个方法提供了对错误的描述。

注意:error 为 nil 代表没有错误

先看一个文件打开错误的例子:

f, err := os.Open("/test.txt")
if err != nil {
    fmt.Println("open failed, err:", err)
    return
}
fmt.Println("file is :", f)

输出:

open failed, err: open /test.txt: The system cannot find the file specified.

可以看到输出了具体错误,分别为:操作 open,操作对象 /test.txt,错误原因 The system cannot find the file specified.

当执行打印错误语句时,fmt 包会自动调用 err.Error() 函数来打印字符串。

这就是错误描述是如何在一行中打印出来的原因。

了解了 error 是什么,我们接下来了解 error 的创建。

二.error 创建#

创建方式有两种:

  • errors.New()
  • fmt.Errorf()

1.errors.New () 函数#

src/errors/errors.go 文件下,定义了 errors.New() 函数,入参为字符串,返回一个 error 对象:

// src/errors/errors.go

// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
    return &errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

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

New () 函数返回一个错误,该错误的格式为给定的文本。
即使文本相同,每次对 New 的调用也会返回一个不同的错误值。

其中 errorString 是一个结构体,只有一个 string 类型的字段 s,并且实现了唯一的方法:Error()

我们实战一下:

// 1.errors.New() 创建一个 error
err1 := errors.New("这是 errors.New() 创建的错误")
fmt.Printf("err1 错误类型:%T,错误为:%v\n", err1, err1)

输出:

err1 错误类型:*errors.errorString,错误为:这是 errors.New() 创建的错误

可以看到,错误类型是 errorString 指针,前面的 errors. 表明了其在 errors 包下。

通常这就够了,它能反映当时 “出错了”,但是有些时候我们需要更加具体的信息。即需要具体的 “上下文” 信息,表明具体的错误值。

这就用到了 fmt.Errorf 函数

2.fmt.Errorf () 函数#

fmt.Errorf() 函数,它先将字符串格式化,并增加上下文的信息,更精确的描述错误。

我们先实战一下,看看和上一节的内容有什么不同:

// 2.fmt.Errorf()
err2 := fmt.Errorf("这个 fmt.Errorf() 创建的错误,错误编码为:%d", 404)
fmt.Printf("err2 错误类型:%T,错误为:%v\n", err2, err2)

输出:

err2 错误类型:*errors.errorString,错误为:这个 fmt.Errorf() 创建的错误,错误编码为:404

可以看到 err2 的类型是 *errors.errorString,并且错误编码 404 也输出了。。

为什么 err2 返回的错误类型也是 :*errors.errorString,我们不是用 fmt.Errorf() 创建的吗?

我们先看下其源码实现:

// src/fmt/errors.go

func Errorf(format string, a ...interface{}) 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.wrappedErrnil 的时候,会调用 errors.New() 来创建错误。

所以 err2 的错误类型是 *errors.errorString 这个问题就解答了。

不过又出现了新问题,这个 p.wrappedErr 是什么东东呢?什么时候为 nil?

我们先看个例子:

// 3. go 1.13 新增加的错误处理特性  %w
err3 := fmt.Errorf("err3: %w", err2)  // err3包裹err2错误
fmt.Printf("err3 错误类型:%T,错误为:%v\n", err3, err3)

输出:

err3 错误类型:*fmt.wrapError,错误为:err3: 这个 fmt.Errorf() 创建的错误,错误编码为:404

注意:在格式化字符串的时候,有一个 %w 占位符,表示格式化的内容是一个 error 类型。

我们主要看下 err3 的内容,其包裹了 err2 错误信息,如下:

err3: 这个 fmt.Errorf() 创建的错误,错误编码为:404

还有一点要注意的是,err3 这次是一个 *fmt.wrapError 类型?这个类型又是源自哪里?怎么会有这样一个类型?又出现了一个新的问题……

好了,带着这些问题,我们从头开始捋一捋源码,就知道它们到底是什么?

我们注意到 fmt.Errorf() 函数第一行 p := newPrinter() 创建了一个 p 对象,这个 p 对象其实就是 pp 结构体指针的实例, newPrinter() 源码如下:

// src/fmt/print.go

// newPrinter allocates a new pp struct or grabs a cached one.
func newPrinter() *pp {
    p := ppFree.Get().(*pp)
    p.panicking = false
    p.erroring = false
    p.wrapErrs = false
    p.fmt.init(&p.buf)
    return p
}

newPrinter() 函数返回一个 pp 结构体指针。

我们看下这个结构体,并看看 p.wrappedErr 字段在该结构体中定义:

// src/fmt/print.go

// pp is used to store a printer's state and is reused with sync.Pool to avoid allocations.
type pp struct {
    ...
    ...
    // wrapErrs is set when the format string may contain a %w verb.
    wrapErrs bool

    // wrappedErr records the target of the %w verb.
    wrappedErr error
}

由于 pp 结构体的字段较多,我们主要看两个字段:

  • wrapErrs 字段,bool 类型,当格式字符串包含%w 动词时,将赋值为 true
  • wrappedErr 字段,error 类型,记录%w 动词的目标,即例子的 err2

所以我们解决了第一问题:p.wrappedErr 到底是什么,什么时候为 nil

即:p.wrappedErr 是 pp 结构体的一个字段,当格式化错误字符串中没有 %w 动词时,其为 nil

还有第二个问题, *fmt.wrapError 类型源自哪里?

其实根源就在 else 语句中,当 p.wrappedErr 不为 nil 时,执行以下语句:

err = &wrapError{s, p.wrappedErr}

err 是结构体 wrapError 的实例,其初始化了两个字段,并且是引用取值(前面有 &)。我们来看看 wrapError 源码:

// src/fmt/errors.go

type wrapError struct {
    msg string
    err error
}

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

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

wrapError 结构体有两个字段:

  • msg ,string 类型
  • err,error 类型

实现了两个方法:

  • Error (),也说明 wrapError 结构体实现了 error 接口,是一个 error 类型
  • Unwrap (),作用是返回原错误值,没有自定义的 msg 了。也就是说拆开了一个被包装的 error。

所以我们的第二个问题, *fmt.wrapError 是什么,就彻底解答了。

至此,捋完 fmt.Errorf() 的源码了,我们了解了想要的内容,至于 p.doPrintf(format, a) 的具体实现内容很复杂,所以就没去深挖了。

总结一下吧,Golang 中创建错误有两种方式:
第一种errors.New() 函数,其返回值类型为 *errors.errorString

第二种fmt.Errorf() 函数
当使用 fmt.Errorf() 来创建错误时,核心有以下两点:

  1. 错误描述中不包含 %w 时,p.wrappedErrnil,所以底层也是调用 errors.New() 创建错误。因此错误类型就是 *errors.errorString

  2. 错误描述中包含%w 时,p.wrappedErr 不为 nil,所以底层实例化 wrapError 结构体指针。 因此错误类型是 *fmt.wrapError,可以理解为包裹错误类型。

本作品采用《CC 协议》,转载必须注明作者和本文链接