这是本节的多页打印视图。 点击此处打印.

返回本页常规视图.

错误处理

Golang 的错误处理

1 - 错误处理概述

Golang 的错误处理概述

go by example

https://gobyexample-cn.github.io/errors

符合 Go 语言习惯的做法是使用一个独立、明确的返回值来传递错误信息。

这与 Java、Ruby 使用的异常(exception) 以及在 C 语言中有时用到的重载 (overloaded) 的单返回/错误值有着明显的不同。

Go 语言的处理方式能清楚的知道哪个函数返回了错误,并使用跟其他(无异常处理的)语言类似的方式来处理错误。

// 按照惯例,错误通常是最后一个返回值并且是 error 类型,它是一个内建的接口。
func f1(arg int) (int, error) { // 
    if arg == 42 {
        // errors.New 使用给定的错误信息构造一个基本的 error 值。
        return -1, errors.New("can't work with 42")
    }
    // 返回错误值为 nil 代表没有错误。
    return arg + 3, nil
}

你还可以通过实现 Error() 方法来自定义 error 类型。 这里使用自定义错误类型来表示上面例子中的参数错误:

// 使用自定义错误类型来表示上面例子中的参数错误
type argError struct {
    arg  int
    prob string
}
// 通过实现 Error() 方法来自定义 error 类型
func (e *argError) Error() string {
    return fmt.Sprintf("%d - %s", e.arg, e.prob)
}

func f2(arg int) (int, error) {
    if arg == 42 {
         // 使用 &argError 语法来建立一个新的结构体
         // 并提供了 arg 和 prob 两个字段的值
         return -1, &argError{arg, "can't work with it"}
    }
    return arg + 3, nil
}

Effective Go

https://golang.org/doc/effective_go.html#errors

类库例程必须经常向调用者返回某种错误指示。如前所述,Go的多值返回使得在返回正常返回值的同时很容易返回一个详细的错误说明。利用这个特性来提供详细的错误信息是一种很好的风格。例如,正如我们将看到的,os.Open 并不只是在失败时返回一个 nil 指针,它还返回一个错误值来描述出了什么问题。

按照惯例,错误的类型为error,是一个简单的内置接口。

type error interface {
    Error() string
}

类库编写者可以自由地用更丰富的模型来实现这个接口,使其不仅可以看到错误,还可以提供一些上下文。如前所述,除了通常的*os.File返回值之外,os.Open还返回一个错误值。如果文件被成功打开,错误值将为nil,但当出现问题时,它将持有一个os.PathError。

// PathError records an error and the operation and
// file path that caused it.
type PathError struct {
    Op string    // "open", "unlink", etc.
    Path string  // The associated file.
    Err error    // Returned by the system call.
}

func (e *PathError) Error() string {
    return e.Op + " " + e.Path + ": " + e.Err.Error()
}

PathError的Error会生成这样一个字符串:

open /etc/passwx: no such file or directory

这样的错误包括有问题的文件名、操作和它所触发的操作系统错误,即使打印出来的时候离引起它的调用很远也是有用的;它比单纯的 “没有这样的文件或目录 “信息量大得多。

在可行的情况下,错误字符串应该识别它们的来源,例如通过有一个前缀来命名产生错误的操作或包。例如,在包image中,由于未知格式导致的解码错误的字符串表示是 “image: unknown format”。

关心精确错误细节的调用者可以使用类型开关(type switch)或类型断言(type assertion)来查找特定错误并提取细节。对于PathErrors来说,这可能包括检查内部Err字段是否存在可恢复的故障。

for try := 0; try < 2; try++ {
    file, err = os.Create(filename)
    if err == nil {
        return
    }
    if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOSPC {
        deleteTempFiles()  // Recover some space.
        continue
    }
    return
}

这里的第二个 if 语句是另一种类型的断言。如果它失败了,ok将为false,e将为nil. 如果它成功了,ok将为true,这意味着错误类型为*os.PathError,那么e也是如此,我们可以检查更多信息。

2 - [博客] 错误处理与go

golang 官方 blog 文章 Error handling and Go

备注:golang官方blog文章 Error handling and Go

介绍

只要写过任何Go代码,就可能遇到过内置的 error 类型。Go代码使用错误值来表示异常状态。例如,os.Open函数在打开文件失败时,会返回一个非零的错误值:

func Open(name string) (file *File, err error)

下面的代码使用 os.Open 来打开一个文件,如果发生错误,则调用 log.Fatal 来打印错误信息并停止。

f, err := os.Open("filename.ext")
if err != nil {
    log.Fatal(err)
}
// do something with the open *File f

只知道错误类型这一点,你就可以在Go中完成很多工作,但在这篇文章中,我们将仔细研究错误,并讨论一些在Go中处理错误的好做法。

error 类型

error 类型是一种接口类型。error 变量代表任何可以描述为字符串的值。下面是接口的声明。

type error interface {
    Error() string
}

与所有内置类型一样,error 类型在宇宙块中预先声明。

最常用的 error 实现是 error package 的未导出的 errorString 类型。

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

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

可以使用 errors.New 函数来构造这些值。它接收一个字符串,并将其转换为 errors.errorString,然后作为一个错误值返回。

// New returns an error that formats as the given text.
func New(text string) error {
    return &errorString{text}
}

下面是如何使用 errors.New:

func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return 0, errors.New("math: square root of negative number")
    }
    // implementation
}

传递负参数给Sqrt的调用者会收到一个非零的错误值(其具体表示是一个error.errorString值)。调用者可以通过调用错误的Error方法来访问错误字符串(“math: square root of…"),或者直接打印它。

f, err := Sqrt(-1)
if err != nil {
    fmt.Println(err)
}

fmt包通过调用 Error() 字符串方法来格式化错误值。

错误实现的责任是总结上下文。os.Open 返回的错误格式为 “open /etc/passwd: permission denied”,而不仅仅是 “permission denied."。我们的Sqrt返回的错误缺少了无效参数的信息。

要添加这些信息,一个有用的函数是fmt包的Errf。它根据Printf的规则格式化一个字符串,并将其作为一个由error.New创建的错误返回。

if f < 0 {
    return 0, fmt.Errorf("math: square root of negative number %g", f)
}

在很多情况下,fmt.Errorf已经足够好了,但是由于error是一个接口,你可以使用任意的数据结构作为错误值,以允许调用者检查错误的细节。

例如,我们假设的调用者可能想恢复传递给Sqrt的无效参数。我们可以通过定义一个新的错误实现而不是使用 errors.errorString 来实现。

type NegativeSqrtError float64

func (f NegativeSqrtError) Error() string {
    return fmt.Sprintf("math: square root of negative number %g", float64(f))
}

然后,复杂的调用者可以使用类型断言来检查NegativeSqrtError,并对其进行特殊处理,而只是将错误传递给fmt.Println或log.Fatal的调用者则不会看到行为的改变。

另一个例子是,json包指定了一个SyntaxError类型,当json.Decode函数在解析JSON blob时遇到语法错误时,它会返回这个类型。

type SyntaxError struct {
    msg    string // description of error
    Offset int64  // error occurred after reading Offset bytes
}

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

Offset字段甚至没有显示在错误的默认格式中,但调用者可以使用它来为他们的错误信息添加文件和行信息。

if err := dec.Decode(&val); err != nil {
    if serr, ok := err.(*json.SyntaxError); ok {
        line, col := findLine(f, serr.Offset)
        return fmt.Errorf("%s:%d:%d: %v", f.Name(), line, col, err)
    }
    return err
}

这是Camlistore项目中一些实际代码的略微简化版本)。

错误接口只需要一个Error方法;特定的错误实现可能有额外的方法。例如,net包按照通常的惯例返回类型为error的错误,但一些错误实现有net.Error接口定义的附加方法。

package net

type Error interface {
    error
    Timeout() bool   // Is the error a timeout?
    Temporary() bool // Is the error temporary?
}

客户端代码可以通过类型断言来测试net.Error,然后区分暂时性的网络错误和永久性的错误。例如,网络爬虫可能会在遇到暂时性错误时休眠并重试,否则就会放弃。

if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
    time.Sleep(1e9)
    continue
}
if err != nil {
    log.Fatal(err)
}

简化重复性错误处理

在Go中,错误处理很重要。该语言的设计和约定鼓励您在错误发生时明确地检查错误(与其他语言中的抛出异常和有时捕获错误的约定不同)。在某些情况下,这使得Go代码变得啰嗦,但幸运的是,您可以使用一些技术来减少重复的错误处理。

考虑一个带有 HTTP 处理程序的 App Engine 应用程序,该处理程序从数据存储中检索一条记录,并使用模板对其进行格式化。

func init() {
    http.HandleFunc("/view", viewRecord)
}

func viewRecord(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    if err := viewTemplate.Execute(w, record); err != nil {
        http.Error(w, err.Error(), 500)
    }
}

这个函数处理由datastore.Get函数和viewTemplate的Execute方法返回的错误。在这两种情况下,它都会向用户呈现一个简单的错误信息,并给出HTTP状态码500(“内部服务器错误”)。这看起来是一个可管理的代码量,但增加一些HTTP处理程序,你很快就会得到许多相同错误处理代码的副本。

为了减少重复,我们可以定义自己的HTTP appHandler类型,其中包括一个错误返回值:

type appHandler func(http.ResponseWriter, *http.Request) error

然后,我们可以改变我们的viewRecord函数来返回错误。

func viewRecord(w http.ResponseWriter, r *http.Request) error {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        return err
    }
    return viewTemplate.Execute(w, record)
}

这比原来的版本更简单,但http包并不理解返回错误的函数。为了解决这个问题,我们可以在appHandler上实现http.Handler接口的ServeHTTP方法。

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if err := fn(w, r); err != nil {
        http.Error(w, err.Error(), 500)
    }
}

ServeHTTP方法调用appHandler函数,并向用户显示返回的错误(如果有的话)。请注意,该方法的接收器 fn 是一个函数,(Go 可以做到这一点!)该方法通过调用表达式 fn(w) 中的接收者来调用函数。(Go 可以做到这一点!) 方法通过调用表达式 fn(w, r) 中的接收者来调用函数。

现在,当我们在http包中注册viewRecord时,我们使用Handle函数(而不是HandleFunc),因为appHandler是一个http.Handler(而不是http.HandlerFunc)。

func init() {
    http.Handle("/view", appHandler(viewRecord))
}

有了这个基本的错误处理基础架构,我们可以让它变得更加友好。与其仅仅显示错误字符串,不如给用户一个简单的错误信息,并附上适当的HTTP状态代码,同时将完整的错误记录到App Engine开发者控制台,以便进行调试。

为此,我们创建一个appError结构,包含一个错误和一些其他字段。

type appError struct {
    Error   error
    Message string
    Code    int
}

接下来我们修改appHandler类型来返回*appError值。

type appHandler func(http.ResponseWriter, *http.Request) *appError

(通常情况下,传回 error 的具体类型而不是 error 是错误的,原因在Go FAQ中讨论过,但在这里是正确的,因为ServeHTTP是唯一能看到该值并使用其内容的地方。)

并让appHandler的ServeHTTP方法将appError的Message以正确的HTTP状态码显示给用户,并将完整的Error记录到开发者控制台。

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if e := fn(w, r); e != nil { // e is *appError, not os.Error.
        c := appengine.NewContext(r)
        c.Errorf("%v", e.Error)
        http.Error(w, e.Message, e.Code)
    }
}

最后,我们将viewRecord更新为新的函数签名,并让它在遇到错误时返回更多的上下文。

func viewRecord(w http.ResponseWriter, r *http.Request) *appError {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        return &appError{err, "Record not found", 404}
    }
    if err := viewTemplate.Execute(w, record); err != nil {
        return &appError{err, "Can't display record", 500}
    }
    return nil
}

这个版本的viewRecord和原来的长度是一样的,但现在每一行都有特定的意义,我们提供的是更友好的用户体验。

这并没有结束,我们还可以进一步改进我们应用程序中的错误处理。一些想法。

  • 给错误处理程序一个漂亮的HTML模板。

  • 当用户是管理员时,通过将堆栈跟踪写入HTTP响应,使调试变得更容易。

  • 为appError写一个构造函数,存储堆栈跟踪以方便调试。

  • 从appHandler内部的恐慌中恢复,将错误记录到控制台中,称为 “Critical”,同时告诉用户 “发生了一个严重的错误”。这是一个很好的触动,避免了让用户暴露在编程错误引起的不可捉摸的错误信息中。更多细节请参见Defer、Panic和Recover文章。

结束语

正确的错误处理是优秀软件的基本要求。通过运用本篇文章中描述的技术,你应该能够写出更可靠、更简洁的Go代码。

3 - [博客] 错误是值

golang 官方 blog 文章 Errors are values

备注:golang官方blog文章 Errors are values

在Go程序员中,尤其是那些刚接触这门语言的程序员,经常讨论的一个问题就是如何处理错误。谈话往往变成了对这个序列的次数的哀叹:

if err != nil {
    return err
}

显示出来。我们最近扫描了所有能找到的开源项目,发现这个片段每一两页只出现一次,比一些人认为的要少。不过,如果人们仍然认为必须键入:

if err != nil

一直以来,一定有什么地方出了问题,而明显的目标是 go 本身。

这是不幸的,误导性的,而且很容易纠正。也许发生的情况是,刚接触 go 的程序员会问:“如何处理错误?"。学会了这种模式,就止步于此。在其他语言中,人们可能会使用 try-catch 块或其他类似机制来处理错误。因此,程序员就会想,我在以前的语言中会使用 try-catch,在Go中我就直接输入 if err != nil。随着时间的推移,Go代码收集了很多这样的片段,结果感觉很笨拙。

不管这种解释是否合适,很明显,这些Go程序员忽略了一个关于错误的基本点:Errors are values (错误是值)。

值可以被编程,既然错误是值,那么错误也可以被编程。

当然,涉及错误值的常见语句是测试它是否为nil,但还有无数其他的事情可以用错误值来做,应用这些其他的一些事情可以让你的程序变得更好,消除了很多如果每个错误都用死板的 if 语句来检查所产生的模板。

下面是一个简单的例子,来自 bufio 包的 Scanner 类型。它的 Scan 方法执行了底层的I/O,这当然会导致错误。然而Scan方法根本没有暴露错误。相反,它返回一个布尔值,并在扫描结束时运行一个单独的方法,报告是否发生错误。客户端代码是这样的。

scanner := bufio.NewScanner(input)
for scanner.Scan() {
    token := scanner.Text()
    // process token
}
if err := scanner.Err(); err != nil {
    // process the error
}

当然,有一个错误的nil检查,但它只出现和执行一次。扫描方法可以被定义为:

func (s *Scanner) Scan() (token []byte, error)

然后示例用户代码可能是(取决于如何检索令牌):

scanner := bufio.NewScanner(input)
for {
    token, err := scanner.Scan()
    if err != nil {
        return err // or maybe break
    }
    // process token
}

这并没有什么不同,但有一个重要的区别。在这段代码中,客户端必须在每次迭代时检查错误,但在真正的 Scanner API 中,错误处理是从关键的 API 元素中抽象出来的,而关键的 API 元素就是迭代 tokens。因此,使用真正的API,客户端的代码感觉更自然:循环直到完成,然后再担心错误。错误处理不会掩盖控制流。

当然,在掩盖之下发生的事情是,一旦Scan遇到一个I/O错误,它就会记录下来并返回false。当客户端询问时,一个单独的方法Err会报告错误值。虽然这很微不足道,但它和把

if err != nil

放的遍地都是,或者要求客户端在每个token之后检查错误。这就是带有错误值的编程。简单的编程,是的,但还是编程。

值得强调的是,无论设计如何,程序检查错误是至关重要的,无论它们是如何暴露的。这里讨论的不是如何避免检查错误,而是如何使用语言优雅地处理错误。

当我参加2014年秋季在东京举行的GoCon时,就出现了重复查错代码的话题。一位在Twitter上化名为@jxck_的热心地鼠对错误检查发出了熟悉的感叹。他有一些代码的示意是这样的。

_, err = fd.Write(p0[a:b])
if err != nil {
    return err
}
_, err = fd.Write(p1[c:d])
if err != nil {
    return err
}
_, err = fd.Write(p2[e:f])
if err != nil {
    return err
}
// and so on

这是非常重复的。在真实的代码中,这段代码比较长,发生的事情比较多,所以不容易只用帮助函数重构,但在这种理想化的形式下,在错误变量上关联一个函数字面量会有帮助:

var err error
write := func(buf []byte) {
    if err != nil {
        return
    }
    _, err = w.Write(buf)
}
write(p0[a:b])
write(p1[c:d])
write(p2[e:f])
// and so on
if err != nil {
    return err
}

这种模式很好用,但需要在每个做write的函数中都有一个闭包;单独的帮助函数使用起来比较笨拙,因为err变量需要在不同的调用中维护(试试)。

我们可以借鉴上面Scan方法的思想,使之更干净、更通用、更可重用。我在我们的讨论中提到了这个技术,但@jxck_并没有看到如何应用它。经过长时间的交流,由于语言障碍,我问是否可以借用他的笔记本,通过输入一些代码给他看。

我定义了一个叫 errWriter 的对象,类似这样。

type errWriter struct {
    w   io.Writer
    err error
}

并给了它一个方法,write。它不需要有标准的 Write 签名,而且它的小写部分是为了突出区别。write方法会调用底层Writer的Write方法,并记录第一个错误,供以后参考。

func (ew *errWriter) write(buf []byte) {
    if ew.err != nil {
        return
    }
    _, ew.err = ew.w.Write(buf)
}

一旦发生错误,写方法就会变成无操作,但错误值会被保存。

给定errWriter类型和它的写法,上面的代码可以重构。

ew := &errWriter{w: fd}
ew.write(p0[a:b])
ew.write(p1[c:d])
ew.write(p2[e:f])
// and so on
if ew.err != nil {
    return ew.err
}

这样做更干净,甚至比使用闭包更干净,也使实际的写入顺序更容易在页面上看到。再也没有杂乱无章的东西了。用错误值(和接口)编程让代码变得更漂亮了。

很有可能在同一个包中的其他代码可以建立在这个想法上,甚至直接使用errWriter。

另外,一旦errWriter存在,它还可以做更多的事情来帮助我们,尤其是在不太人为的例子中。它可以累积字节数。它可以将写入的内容凝聚成一个单一的缓冲区,然后可以原子化地传输。还有更多。

事实上,这种模式经常出现在标准库中。archive/zip和net/http包都使用了它。更突出的是,bufio包的Writer实际上是errWriter思想的一个实现。虽然bufio.Writer.Write会返回一个错误,但那主要是为了尊重io.Writer接口。bufio.Writer的Write方法的行为就像我们上面的errWriter.write方法一样,Flush会报告错误,所以我们的例子可以这样写。

b := bufio.NewWriter(fd)
b.Write(p0[a:b])
b.Write(p1[c:d])
b.Write(p2[e:f])
// and so on
if b.Flush() != nil {
    return b.Flush()
}

这种方法有一个重大的缺点,至少对某些应用来说是这样:无法知道错误发生前完成了多少处理。如果该信息很重要,就需要采用更细化的方法。不过,通常情况下,在最后进行全有或全无的检查就足够了。

我们只看了一种避免重复性错误处理代码的技术。请记住,使用 errWrite r或 bufio.Writer 并不是简化错误处理的唯一方法,而且这种方法并不适合所有情况。然而,关键的经验是,错误是值,可以利用Go编程语言的全部能力来处理它们。

使用该语言来简化你的错误处理。

但请记住。无论你做什么,总是要检查你的错误!

4 - [博客] go1.13中的错误处理

golang 官方 blog 文章 Working with Errors in Go 1.13

备注:golang官方blog文章 Working with Errors in Go 1.13

介绍

在过去的十年里,Go 将错误处理为值(errors as values)的做法对我们很有帮助。虽然标准库对错误的支持很少–只有 errors.New 和 fmt.Errorf 函数,它们产生的错误只包含一条消息–但内置的 error 接口允许 Go 程序员添加他们想要的任何信息。它所需要的只是一个实现 Error 方法的类型。

type QueryError struct {
    Query string
    Err   error
}

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

像这样的错误类型无处不在,它们所存储的信息也千差万别,从时间戳到文件名到服务器地址。通常,这些信息包括另一个低级错误,以提供额外的上下文。

一个错误包含另一个错误的模式在 Go 代码中是如此普遍,以至于经过广泛的讨论,Go 1.13 增加了对它的明确支持。这篇文章描述了标准库中提供这种支持的新增内容:错误包中的三个新函数,以及 fmt.Errorf 的新格式动词。

在详细描述这些变化之前,让我们先回顾一下在以前的语言版本中是如何检查和构造错误的。

go 1.13之前的错误

检查错误

Go error 是值。程序根据这些值以几种方式做出决定。最常见的是将错误与nil进行比较,以确定操作是否失败。

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

有时我们会将错误与已知的 sentinel 值进行比较,看看是否发生了特定的错误。

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

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

错误值可以是满足语言定义的 error 接口的任何类型。程序可以使用类型断言或类型转换来将错误值视为更具体的类型。

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
}

添加信息

经常有函数在调用堆栈中传递错误,同时在其中添加信息,比如错误发生时的简要描述。一个简单的方法是构造新的错误,其中包括前一个错误的文本。

if err != nil {
    return fmt.Errorf("decompress %v: %v", name, err)
}

使用 fmt.Errorf 创建一个新的错误,会丢弃原始错误中除文本以外的所有内容。正如我们在上面的QueryError中所看到的,我们有时可能希望定义一个新的错误类型,其中包含底层错误,保留它以便于代码检查。这里又是QueryError。

type QueryError struct {
    Query string
    Err   error
}

程序可以在 *QueryError 值内部查看,根据底层 error 做出决定。你有时会看到这被称为 “解包 “错误。

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

标准库中的os.PathError类型是另一个例子,它包含了一个错误。

Errors in Go 1.13

Unwrap 方法

Go 1.13 为 errors 和 fmt 标准库包引入了新特性,以简化对包含其他错误的错误的处理。其中最重要的是约定,而不是变化:包含另一个error的 error 可以实现一个返回底层 error 的 Unwrap 方法。如果 e1.Unwrap() 返回 e2,那么我们就说 e1 封装(wrap)了e2,可以解开 (unwrap) e1 得到 e2。

按照这个约定,我们可以给上面的 QueryError 类型一个 Unwrap 方法,返回其包含的错误。

func (e *QueryError) Unwrap() error { return e.Err }

使用 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 函数测试 error 是否为特定类型:

// 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 函数的行为就像类型断言。然而,当对封装的错误进行操作时,这些函数会考虑链中的所有error。让我们再看看上面的例子,即解开一个 QueryError 来检查底层错误。

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

使用 errors.Is 函数,我们可以把它写成:

if errors.Is(err, ErrPermission) {
    // err, or some error that it wraps, is a permission problem
}

errors 包还包括一个新的 Unwrap 函数,它返回调用 error 的 Unwrap 方法的结果,如果错误没有Unwrap方法,则返回 nil。但通常最好使用 errors.Is 或 errors.As,因为这些函数会在一次调用中检查整个链。

使用 %w 封装错误

如前所述,通常使用 fmt.Errorf 函数为错误添加附加信息。

if err != nil {
    return fmt.Errorf("decompress %v: %v", name, err)
}

在Go 1.13中,fmt.Errorf 函数支持一个新的 %w 动词。当这个动词存在时,fmt.Errorf 返回的错误将有一个 Unwrap 方法返回 %w 的参数,它必须是一个error。在所有其他方面,%w 与 %v 相同:

if err != nil {
    // Return an error which unwraps to err.
    return fmt.Errorf("decompress %v: %w", name, err)
}

用 %w 包装 error ,使得它可以被 errors.Is 和 erros.As 使用。

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

是否封装

当向 error 添加额外的上下文时,无论是使用 fmt.Errorf 还是通过实现自定义类型,您都需要决定新的 error 是否应该封装原始 error。这个问题没有唯一的答案,它取决于创建新 error 的上下文。封装一个error 以将其暴露给调用者。如果这样做会暴露实现细节,则不要包装error。

举个例子,想象一个从 io.Reader.Reader 中读取复杂数据结构的 Parsse 函数。如果发生错误,我们希望报告发生错误的行号和列号。如果错误是在从 io.Reader 读取时发生的,我们希望对该错误进行包装,以便检查潜在的问题。由于调用者向函数提供了 io.Reader,所以暴露它所产生的错误是有意义的。

相反,一个对数据库进行多次调用的函数可能不应该返回一个对其中一次调用结果进行解包的错误。如果函数使用的数据库是一个实现细节,那么暴露这些错误就违反了抽象性。例如,如果你的包pkg的LookupUser函数使用了Go的数据库/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) 

这时,如果你不想破坏你的客户端,即使你切换到不同的数据库包,函数必须总是返回 sql.ErrNoRows 。换句话说,包装一个错误使该错误成为你的API的一部分。如果你不想承诺在未来支持该错误作为你的API的一部分,你就不应该包装该错误。

重要的是要记住,无论你是否封装,错误文本都是一样的。试图理解该错误的人无论用哪种方式都会得到相同的信息;选择封装是为了给程序提供额外的信息,以便他们能够做出更明智的决定,还是为了保留抽象层而不提供该信息。

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

errors.Is 函数检查链中每个错误是否与目标值匹配。默认情况下,如果两者相等,则错误与目标值匹配。此外,链中的错误可以通过实现 Is 方法声明它与目标值匹配。

作为一个例子,考虑这个错误的灵感来自 Upspin 错误包,它将错误与模板进行比较,只考虑模板中非零的字段。

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".
}

errors.As 函数同样在存在的情况下咨询 As 方法。

Errors 和包API

返回错误的包(大多数都是这样)应该描述这些错误的属性,程序员可以依赖这些属性。一个设计良好的包也会避免返回具有不应该依赖的属性的错误。

最简单的规范是说,操作要么成功,要么失败,分别返回一个 nil 或 non-nil 的错误值。在很多情况下,不需要进一步的信息。

如果我们希望函数返回一个可识别的错误条件,比如 “item not found”,我们可能会返回一个包裹着 sentinel 的 error。

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

还有其他现有的模式可以提供可以被调用者进行语义检查的错误,例如直接返回一个哨兵值、一个特定的类型或一个可以用谓词函数检查的值。

在所有情况下,都应该注意不要向用户暴露内部细节。正如我们在上面的 “是否封装” 中提到的,当你从另一个包中返回一个错误时,你应该将错误转换为不暴露底层错误的形式,除非你愿意承诺在将来返回那个特定的错误。

f, err := os.Open(filename)
if err != nil {
    // The *os.PathError returned by os.Open is an internal detail.
    // To avoid exposing it to the caller, repackage it as a new
    // error with the same text. We use the %v formatting verb, since
    // %w would permit the caller to unwrap the original *os.PathError.
    return fmt.Errorf("%v", err)
}

如果函数被定义为返回一个包裹着某个哨兵或类型的 error ,不要直接返回底层错误。

var ErrPermission = errors.New("permission denied")

// DoSomething returns an error wrapping ErrPermission if the user
// does not have permission to do something.
func DoSomething() error {
    if !userHasPermission() {
        // If we return ErrPermission directly, callers might come
        // to depend on the exact error value, writing code like this:
        //
        //     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:
        //
        //     if err := pkg.DoSomething(); errors.Is(err, pkg.ErrPermission) { ... }
        return fmt.Errorf("%w", ErrPermission)
    }
    // ...
}

总结

虽然我们所讨论的变化仅仅是三个函数和一个格式化动词,但我们希望它们将大大改善Go程序中的错误处理方式。我们希望通过包装来提供额外的上下文会变得很普遍,帮助程序做出更好的决策,帮助程序员更快地找到错误。

正如Russ Cox在GopherCon 2019的主题演讲中所说,在通往Go 2的道路上,我们进行实验、简化和出货。现在我们已经出货了这些变化,我们期待着接下来的实验。