Golang中Error作为返回值是很常见的,几乎每个函数返回值都有error的interface。本文翻译自Error Handling In Go, Part I,作者在该篇中对error接口的创建、使用等做了详细描述。

简介

在Go语言中,使用error接口作为函数或者方法的返回值是一种很惯用的方法。这个接口同样在Go的标准库中也作为返回值。

例如,这是http包的Get方法声明:

Listing 1.1

func (c *Client) Get(url string) (resp *Response, err error)

一般都会在函数、方法的返回中,判断error的值是否为nil:

Listing 1.2

resp, err := c.Get("http://goinggo.net/feeds/posts/default")
if err != nil {
    log.Println(err)
    return
}

在 listing 1.2, 对Get方法的调用通过两个局部变量返回。 然后比较err变量是否等于nil。 如果不等,则表示有错误。

因为error是用于处理错误的接口,我们需要根据提供的接口实现具体的代码。标准库中errorString已经实现了该接口。

Error接口与errString结构

error的接口声明如下:

Listing 1.3

type error interface {
Error() string
}

在 listing 1.3中, 我们看到err接口的声明仅仅包括一个Error, 该方法返回string类型。因此,任何类型只要实现了Error方法,也就实现了err接口。 关于Go中的接口,可以参考Methods, Interfaces and Embedded Types in Go

标准库中也声明了errorString的结构,该结构在errors包中:

Listing 1.4

http://golang.org/src/pkg/errors/errors.go
type errorString struct {
 s string
}

在 listing 1.4中, 我们看到errorString中只有一个类型为string的字段。我们能够看到结构以及内部的字段都是不能够被外部访问的,我们不能直接访问该结构或者它内部的变量,具体可以参考Exported/Unexported Identifiers in Go

Listing 1.5

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

我们可以看到listing 1.5中,error接口通过指针类型的接受者(*errorString)实现了。errorString类型是标准库中最常用的类型,它能够作为error类型接口的返回值。

下面我们来学习标准库中如何通过errorString结构来创建error类型的接口。

创建Error值

标准库中,提供了两种方式来创建errorString类型的指针,以作为error类型的接口使用。当你定义的error是一个string类型,且不需要具体的格式化参数,那么可以通过errors包的New函数定义。

Listing 1.6

var ErrInvalidParam = errors.New("mypackage: invalid parameter")

Listing 1.6中, 显示了errors包的New函数的调用。在这个示例中,我们声明了一个error类型的接口变量,然后通过调用New函数初始化该变量。下面为New的实现:

Listing 1.7

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

Listing 1.7中, 我们看到函数以string类型作为参数传入,返回了error类型的接口。在该函数的实现中,创建了一个errorString类型的指针。 在return语句中,编译器创建了error类型的接口,然后结合指针,作为返回。 errorString指针作为隐含的数据,当做error接口的值返回了。

那么问题来了,如果我们的错误消息需要格式化呢? 不着急,下面的 fmt包中的Errorf函数能够做到。

Listing 1.8

var ErrInvalidParam = fmt.Errorf("invalid parameter [%s]", param)

Listing 1.8中, 我们可以看到Errorf函数被调用了。如果你对fmt包中的其他函数熟悉,你就知道这个函数跟其他函数是类似的了。这里,通过调用Errorf函数, 我们再次创建并初始化了一个error类型的接口变量。

下面我们揭开Errorf函数的神秘面纱

Listing 1.9

http://golang.org/src/pkg/fmt/print.go

// Errorf formats according to a format specifier and returns the string
// as a value that satisfies error.
func Errorf(format string, a …interface{}) error {
    return errors.New(Sprintf(format, a…))
}

Listing 1.9中, 我们看到依然是通过error接口作为返回值类型的。 在该函数的实现中,调用了errors包的New函数,其中参数为格式化好的字符串。所以不管你使用errors包或者fmt包来创建error类型的接口,底层都是为errorString类型的指针

现在我们有两种不同的方式,都能通过errorString的指针实现error类型的接口。下面我们来学习在标准库中,如何在API调用中返回特有的errors信息。

比较Errors值

bufio包中(标准库中其他包也是一样的),通过errors包中的New函数来创建不同error变量。

Listing 1.10

http://golang.org/src/pkg/bufio/bufio.go

var (
    ErrInvalidUnreadByte = errors.New("bufio: invalid use of UnreadByte")
    ErrInvalidUnreadRune = errors.New("bufio: invalid use of UnreadRune")
    ErrBufferFull        = errors.New("bufio: buffer full")
    ErrNegativeCount     = errors.New("bufio: negative count")
)

Listing 1.10 展示了四个不同的error变量,它们都在bufio包中声明、初始化。注意到这些变量都是通过Err作为前缀的,在Go中这是一种约定俗成的书写方式。因为这些变量都被声明为error类型接口,我们能够区分指定的错误,这些错误可以是由bufio包中不同的API返回的:

Listing 1.11

data, err := b.Peek(1)
if err != nil {
    switch err {
    case bufio.ErrNegativeCount:
        // Do something specific.
        return
    case bufio.ErrBufferFull:
        // Do something specific.
        return
    default:
        // Do something generic.
        return
    }
}

在 listing 1.11 中,示例代码调用了bufio.Reader类型指针值的Peek方法。Peek方法返回的值可能是ErrNegativeCount或是 ErrBufferFull变量。 因为这些变量已经对外暴露了,所以我们能够利用这些变量来区分具体是哪个错误。区分这些变量也是标准库中错误处理的一部分。

设想如果我们没有声明这些error变量,那么我们不得不通过比较具体的错误信息来判断我们获得的是那些错误:

Listing 1.12

data, err := b.Peek(1)
if err != nil {
    switch err.Error() {
    case "bufio: negative count":
        // Do something specific.
        return
    case "bufio: buffer full":
        // Do something specific.
        return
    default:
        // Do something specific.
        return
    }
}

在listing 1.12的例子中,有两个问题:首先,Error()函数的调用会产生一份错误消息的拷贝;其次,如果包的开发者改变了这些消息内容,那么这段代码就有问题了。

下面的io包是另外一个例子,声明了error类型的变量,并且都能够作为错误返回:

Listing 1.13

var ErrShortWrite    = errors.New("short write")
var ErrShortBuffer   = errors.New("short buffer")
var EOF              = errors.New("EOF")
var ErrUnexpectedEOF = errors.New("unexpected EOF")
var ErrNoProgress    = errors.New("multiple Read calls return no data or error")

Listing 1.13显示了在io包中的六个error变量。其中第三个变量是EOF错误变量的声明,表示没有多余的输入变量了。通常会在这个包中比较将函数返回的错误值与该值进行比较。

下面是io包中的ReadAtLeast函数的实现:

Listing 1.14

func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error) {
    if len(buf) < min {
        return 0, ErrShortBuffer
    }
    for n < min && err == nil {
        var nn int
        nn, err = r.Read(buf[n:])
        n += nn
    }
    if n >= min {
        err = nil
    } else if n > 0 && err == EOF {
        err = ErrUnexpectedEOF 
    }
    return
}

在listing 1.14中,ReadAtLeast函数显示了如何使用这些错误变量。这里要注意到 ErrShortBufferErrUnexpectedEOF是如何作为返回值的。 同样需要注意到函数如何比较err变量与EOF变量的。

在你自己写的API中,创建错误类型变量,需要考虑是否有必要自己是实现,这也能使你提升错误处理的能力。

为何不是值类型

这里,也许会想到一个问题,为啥Go语言的设计者不直接设计一个errString的值类型,而是使用结构类型?

值类型可以定义为如下方式:

type errorString string

结构类型定位为如下方式:

type errorString struct {
 s string
}

下面给出命名类型的具体实现例子:

Listing 1.15

01 package main
02 
03 import (
04     "errors"
05     "fmt"
06 )
07
08 // Create a named type for our new error type.
09 type errorString string
10
11 // Implement the error interface.
12 func (e errorString) Error() string {
13     return string(e)
14 }
15
16 // New creates interface values of type error.
17 func New(text string) error {
18     return errorString(text)
19 }
20
21 var ErrNamedType = New("EOF")
22 var ErrStructType = errors.New("EOF")
23
24 func main() {
25     if ErrNamedType == New("EOF") {
26         fmt.Println("Named Type Error")
27     }
28
29     if ErrStructType == errors.New("EOF") {
30         fmt.Println("Struct Type Error")
31     }
32 } 

Output:
Named Type Error

Listing 1.15 提供了简单的示例,显示了errorString作为值类型的错误使用。在09行声明了一个string的值类型,在12行,error接口通过命名类型是吸纳。在17行实行了New函数定义。

在21和22行,定义、初始化了两种不同类型的变量。分别通过New函数和 errors.New函数进行初始化。最后,在main()函数中,与相同的方式创建的变量进行了比较。

当你运行这段代码时,得到了上述有趣的结果。25行if条件成立,而29行if条件不成立。通过使用命名类型,我们能够创建error类型值得接口,而且如果错误信息相同,那么就会匹配。这导致的问题其实和1.12l类似,因为我们能够创建自己的error值,并且使用它们。如果是通过值类型创建的error,那么包的作者改变错误消息的话,我们的代码判断就出问题了。

同样我们也能通过errorString的结构体类型复现上面的问题,只要接受者为值(T, 而不是 *T), 实现error接口如下:

Listing 1.16

01 package main
02
03 import (
04    "fmt"
05 )
06
07 type errorString struct {
08    s string
09 }
10
11 func (e errorString) Error() string {
12    return e.s
13 }
14
15 func NewError(text string) error {
16    return errorString{text}
17 }
18
19 var ErrType = NewError("EOF")
20
21 func main() {
22    if ErrType == NewError("EOF") {
23        fmt.Println("Error:", ErrType)
24    }
25 } 

Output:
Error: EOF

在listing 1.16中,我们实现了errorString的结构体类型,该类型对于error接口的实现用的接受者是 errorString, 而非* errorString. 这一次得到的结果正如 listing 1.15 一样,他们真正比较的其实是具体类型中的值。11-13行代码 可以与 listing 1.9 进行比较,值得回味哈。

在标准库中,** 使用* errorString作为error接口的实现的接受者 ** ,errors.New函数强制返回了指针值, 这个指针就是绑定接口的值,并且每次调用都是指向同一个对象。这种情况下比较的其实是指针的值,而不是真正的错误消息。

总结

该篇文章中,我们初步理解了error接口是什么,如何与errorString结构相结合的。 通过errors.Newfmt.Errorf函数来创建error类型的接口值,这种方式是非常普遍并且也是强烈推荐的。

我们也可以暴露(对外能否访问)我们定义的error类型的接口值(通常是标准库定义的),它能够通过API调用返回,帮助我们识别不同的错误信息。很多标准库的包中创建了这些对外可以访问的error变量,这些通常已经提供了足够的识粒度来区分不同的错误信息。

有时我们需要自己创建合理的error类, 这些将会在第二部分中讲述。现在,请使用标准库提供的支持来处理错误吧!