Go 1.13中错误处理的新特性,你用起来了吗?

1,788 阅读7分钟

英文原文发布于blog.golang.org,blog.golang.org/go1.13-erro…

由于原文较长,在这里只总结出精华部分,如有需要可查看原文。

Golang标准库对于错误处理机制的支持一直是开发者所诟病的,仅有errors.New和fmt.Errorf函数且产生的错误包含的信息量有限。即便开发者可以通过实现内置的Error interface添加自定义的错误信息:

type QueryError struct {
    Query string
    Err   error
}
func (e *QueryError) Error() string { return e.Query + ": " + e.Err.Error() }

Go代码中像这样的错误处理无处不在,包含的错误信息有时间戳、文件名、server等。通常,该信息包括另一个较低级别的错误以提供上下文信息。

在Go代码中,使用一个包含了另一个错误的错误类型的模式十分普遍,所以经过Go team的讨论后,Go 1.13为其添加了一些错误处理的支持。这篇博文解读了标准库提供的错误处理新特性:errors包中的三个新功能(Unwrap,As,Is),以及fmt.Errorf中添加的新格式化动词 %w

                        *快速解读*

1.13之前的错误处理

由于go的错误是值(errors are values)类型,1.13之前的错误处理方式是程序基于这些值来做出决策:

通过与nil的比较来确定操作是否失败;

if err != nil {
    // something went wrong
}

将错误与已知的哨兵值(sentinel value)进行比较来查看是否发生了特定错误

var ErrNotFound = errors.New("not found")

if err == ErrNotFound {
    // something wasn't found
}

错误值可以是满足定义的error 接口的任何类型。程序可以使用类型断言(type assertion)或类型开关(type switch)来判断错误值是否可被视为特定的错误类

type NotFoundError struct {
    Name string
}

func (e *NotFoundError) Error() string { return e.Name + ": not found" }

if e, ok := err.(*NotFoundError); ok {
    // e.Name wasn't found
}

Go 1.13版本的错误处理

Unwrap方法

包含另一个错误的错误值可以实现Unwrap方法来返回所包含的底层错误。如果e1.Unwrap()返回了e2,那么我们说e1包装了e2,可以Unwrap e1来得到e2。为上面的QueryError类型提供一个Unwrap方法来返回其包含的错误 func (e *QueryError) Unwrap() error { return e.Err } Unwrap错误的结果本身(底层错误)可能也具有Unwrap方法,这种通过重复unwrap而得到的错误序列称为错误链。 使用Is和As检查错误 Go 1.13的errors包中包括了两个用于检查错误的新函数:Is和As。 errors.Is函数将错误与值进行比较。

// Similar to:
//   if err == ErrNotFound { … }
if errors.Is(err, ErrNotFound) {
    // something wasn't found
}

As函数用于测试错误是否为特定类型。

// Similar to:
//   if e, ok := err.(*QueryError); ok { … }
var e *QueryError
if errors.As(err, &e) {
    // err is a *QueryError, and e is set to the error's value
}
在最简单的情况下,errors.Is函数的行为类似于上面对哨兵错误(sentinel error))的比较,而errors.As函数的行为类似于类型断言(type assertion)。但是,在处理包装错误(包含其他错误的错误)时,这些函数会考虑错误链中的所有错误。让我们再次看一下通过展开QueryError以检查潜在错误:
```go

if e, ok := err.(*QueryError); ok && e.Err == ErrPermission { // query failed because of a permission problem }

### 使用errors.Is函数,我们可以这样写:
```go
if errors.Is(err, ErrPermission) {
    // err, or some error that it wraps, is a permission problem
}

用%w包装错误。

我们通常使用fmt.Errorf函数向错误添加其他信息

if err != nil {
return fmt.Errorf("decompress %v: %v", name, err)
}
在Go 1.13中,fmt.Errorf函数支持新的%w动词。当存在该动词时,所返回的错误fmt.Errorf将具有Unwrap方法,该方法返回参数%w对应的错误。%w对应的参数必须是错误(类型)。在其他方面,%w与%v用法相同。
if err != nil {
    // Return an error which unwraps to err.
    return fmt.Errorf("decompress %v: %w", name, err)
}

使用%w创建的包装错误可用于errors.Is和errors.As:

err := fmt.Errorf("access denied: %w”, ErrPermission)
...
if errors.Is(err, ErrPermission) ...

是否对错误进行包装

在使用fmt.Errorf或通过实现自定义类型将其他上下文包装进Error中时,需要确定新错误是否应该包装原始错误。这个问题没有统一答案。它取决于创建新错误的上下文,包装错误将会被公开给调用者,如果要避免暴露实现细节,那么请不要包装错误。 举一个例子,假设一个Parse函数从io.Reader读取复杂的数据结构。如果发生错误,我们希望报告发生错误的行号和列号。如果从io.Reader读取时发生错误,我们将包装该错误以供检查底层问题。由于调用者为函数提供了io.Reader,因此有理由公开它产生的错误。 相反,一个对数据库进行多次调用的函数,不应该将其调用的结果的错误返回。例如,如果你的程序包pkg中的函数LookupUser使用了Go的database/sql程序包,则可能会遇到sql.ErrNoRows错误。如果使用fmt.Errorf("accessing DB: %v", err)来返回该错误,则调用者无法检查到内部的sql.ErrNoRows。但是,如果函数使用fmt.Errorf("accessing DB: %w", err)返回错误,则调用者可以编写下面代码:

err := pkg.LookupUser(...)
if errors.Is(err, sql.ErrNoRows) …

使用Is和As方法自定义错误测试

errors.Is函数检查错误链中的每个错误是否与目标值匹配。默认情况下,如果两者相等,则错误与目标匹配。另外,链中的错误也会会通过实现Is方法来声明它与目标是否匹配。 例如,下面的错误类型定义,它将错误与模板进行了比较,并且仅考虑模板中非零的字段:

type Error struct {
    Path string
    User string
}
 
func (e *Error) Is(target error) bool {
    t, ok := target.(*Error)
    if !ok {
        return false
    }
    return (e.Path == t.Path || t.Path == "") &&
           (e.User == t.User || t.User == "")
}
 
if errors.Is(err, &Error{User: "someuser"}) {
    // err's User field is "someuser".
}
同样,如果该错误实现了As方法,errors.As函数可以使用链中某个错误的As方法。

package API的错误处理 返回值中含有错误值的package API(大多数都会返回错误)应描述程序员可能依赖的那些错误的属性。一个经过精心设计的package也将避免返回带有不应依赖的错误值。 最简单的规约是用于说明操作成功或失败的属性,分别返回nil或non-nil错误值。在许多情况下,不需要进一步的信息了。如果我们希望函数返回可识别的错误条件,例如“item not found”。

var ErrNotFound = errors.New("not found")

// FetchItem returns the named item.
//
// If no item with the name exists, FetchItem returns an error
// wrapping ErrNotFound.
func FetchItem(name string) (*Item, error) {
  if itemNotFound(name) {
      return nil, fmt.Errorf("%q: %w", name, ErrNotFound)
  }
  // ...
}
如果将函数定义为返回包装某些标记或类型的错误,请不要直接返回基础错误。
var ErrPermission = errors.New("permission denied")
 
// DoSomething returns an error wrapping ErrPermission if the user
// does not have permission to do something.
func DoSomething() {
    if !userHasPermission() {
        // If we return ErrPermission directly, callers might come
        // to depend on the exact error value, writing code like this:
        //如果我们直接返回ErrPermission,那么调用者就要依赖确切的error值,写出下面的代码:
        //     if err := pkg.DoSomething(); err == pkg.ErrPermission { … }
        //
        // This will cause problems if we want to add additional
        // context to the error in the future. To avoid this, we
        // return an error wrapping the sentinel so that users must
        // always unwrap it:
        //这样就会导致,如果我们想给这个ErrPermission添加一些东西,就会影响到调用者
        //为了避免这种情况,我们需要返回一个包装ErrPermission的错误值,调用者就可以用error.Is来判断,
        //而不用关心ErrPermission的内部信息
        //     if err := pkg.DoSomething(); errors.Is(err, pkg.ErrPermission) { ... }
        return fmt.Errorf("%w", ErrPermission)
    }
    // ...
}

最后

尽管本文讨论的更改仅包含三个函数和一个格式化动词(%w),但Go team希望它们能大幅改善Go程序中错误处理的方式。官方希望通过包装来提供其他上下文的方式让Gopher更加高效地处理错误,并且更快地发现错误。 紧跟Go官方blog的步伐,追踪热点资讯,尽在go official blog。

如发现文中错误,可至公众号反馈(也欢迎技术交流),一经采纳有红包🧧奉上哟。