Error Handling In Go, Part I

Error Handling In Go, Part I

Introduction

转自:

http://www.goinggo.net/2014/10/error-handling-in-go-part-i.html


It is idiomatic in Go to use the error interface type as the return type for any error that is going to be returned from a function or method. This interface is used by all the functions and methods in the standard library that return errors. For example, here is the declaration for the Get method from the http package: 


  Listing 1.1  
http://golang.org/pkg/net/http/#Client.Get

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

Listing 1.1 shows how the second return argument for the  Get  method is an interface value of type  error . Handling errors that are returned from functions and methods starts by checking if the returned interface value of type  error  is not  nil :

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

In listing 1.2, a call to  Get  is performed and the return values are assigned to local variables. Then the value of the err  variable is compared to the value of  nil . If the value is not  nil , then there was an error.

Because an interface is being used to handle error values, a concrete type needs to be declared that implements the interface. The standard library has declared and implemented this concrete type for us in the form of a struct called  errorString . In this post, we will explore the implementation and use of the  error  interface and errorString  struct from the standard library. 

Error Interface and errorString Struct
The declaration of the  error  interface is provided to us by the language directly: 

  Listing 1.3  
http://golang.org/pkg/builtin/#error

type error interface {
    Error() string
}

In listing 1.3, we can see how the  error  interface is declared with a single method called  Error  that returns a string . Therefore, any type that implements the  Error  method will implement the interface and can be used as an interface value of type  error . If you are not familiar with how interfaces work in Go, read my post about Interfaces, Methods and Embedded Types .

The standard library has also declared a struct type called  errorString  that can be found in the  errors package: 

  Listing 1.4  
http://golang.org/src/pkg/errors/errors.go

type errorString struct {
    s string
}

In listing 1.4, we can see how the declaration of  errorString  shows a single field named  s  of type  string . Both the type and its single field are unexported, which means we can’t directly access the type or its field. To learn more about unexported identifiers in Go, read my post about  Exported/Unexported Identifiers in Go

The  errorString  struct implements the error interface: 

  Listing 1.5  
http://golang.org/src/pkg/errors/errors.go

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

The  error  interface is implemented with a pointer receiver as seen in listing 1.5. This means only pointers of type errorString  can be used as an interface value of type  error . Also, since the  errorString  type and its single field are unexported, we can’t perform a type assertion or conversion of the  error  interface value. Our only access to the value of the concrete type is with the  Error  method.

The  errorString  type is the most common type of error that is returned as an interface value of type  error within the standard library. Now that we know what these types look like, let’s learn how the standard library gives us the ability to create an interface value of type  error  using the  errorString  struct.

Creating Error Values
The standard library provides two ways to create pointers of type  errorString  for use as an interface value of type  error . When all you need for your error is a string with no special formatting, the  New  function from the errors  package is the way to go: 

  Listing 1.6  
var ErrInvalidParam = errors.New(“mypackage: invalid parameter”)

Listing 1.6 shows a typical call to the  New  function from the  errors  package. In this example, an interface variable of type  error  is declared and initialized after the call to  New . Let’s look at the declaration and implementation of the New  function:

  Listing 1.7  
http://golang.org/src/pkg/errors/errors.go

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

In the declaration of the  New  function in listing 1.7, we see the function takes a  string  as a parameter and returns an interface value of type  error . In the implementation of the function, a pointer of type  errorString  is created. Then on the return statement, an interface value of type  error  is created by the compiler and bound to the pointer to satisfy the return argument. The  errorString  pointer becomes the underlying data value and type for the interface  error  value that is returned.

When you have an error message that requires formatting, use the  Errorf  function from the  fmt  package: 

  Listing 1.8  
var ErrInvalidParam = fmt.Errorf(“invalid parameter [%s]”, param)

Listing 1.8 shows a typical call to the  Errorf  function. If you are familiar with using the other format functions from the  fmt  package, then you will notice this works the same. Once again, an interface variable of type  error  is declared and initialized after the call to  Errorf .

Let’s look at the declaration and implementation of the  Errorf  function: 

  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…))
}

In the declaration of the  Errorf  function in listing 1.9, we see the  error  interface type is being used once again as the return type. In the implementation of the function, the  New  function from the  errors  package is used to create an interface value of type  error  for the message that is formatted. So whether you use the  errors  or  fmt package to create your interface value of type  error , the value underneath is always a pointer of type errorString .

Now we know the two different ways we can create interface values of type  error  using a pointer of type errorString . Next, let’s learn how packages in the standard library provide support for comparing unique errors that are returned from API calls. 

Comparing Error Values
The  bufio  package, like many other packages in the standard library, uses the  New  function from the  errors package to create package level error variables: 

  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 shows four package level error variables that are declared and initialized in the  bufio  package. Notice each error variable starts with the prefix  Err . This is a convention in Go. Since these variables are declared as interface type  error , we can use these variables to identify specific errors that are returned by the different  bufio package API’s:

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

In listing 1.11, the code example calls into the  Peek  method from a pointer variable of type  bufio.Reader . The Peek  method has the potential of returning both the  ErrNegativeCount  and  ErrBufferFull  error variables. Because these variables have been exported by the package, we now have the ability to use the variables to identify which specific error message was returned. These variable become part of the packages API for error handling.

Imagine if the  bufio  package did not declare these error variables. Now we would need to compare the actual error messages to determine which error we received: 

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

There are two problems with the code example in listing 1.12. First, the call to  Error()  requires a copy of the error message to be made for the  switch  statement. Second, if the package author ever changes these messages, this code breaks.

The  io  package is another example of a package that declares interface type  error  variables for the errors that can be returned: 

  Listing 1.13  
http://golang.org/src/pkg/io/io.go

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 shows six package level error variables that are declared in the  io  package. The third variable is the declaration of the  EOF  error variable that is returned to indicate when there is no more input available. It is common to compare the error value from functions in this package with the  EOF  variable.

Here is the implementation of the  ReadAtLeast  function from inside  io  package: 

  Listing 1.14  
http://golang.org/src/pkg/io/io.go

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
}

The  ReadAtLeast  function in listing 1.14 shows the use of these error variables in action. Notice how the error variables  ErrShortBuffer  and  ErrUnexpectedEOF  are used as a return value. Also notice how the function compares the  err  variable against the  EOF  variable, just like we do in our own code.

This pattern of creating error variables for the errors your API’s are going to return is something you should consider implementing yourself. It helps provide an API for errors and keeps error handling performant. 

Why Not a Named Type
One question that comes up is why didn’t the language designers use a named type for  errorString ?

Let’s take a look at the implementation using a named type and compare it to using the struct type:

  Listing 1.15  
http://play.golang.org/p/uZPi4XKMF9

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 provides a sample program to show a problem surrounding the use of a named type for  errorString . The program on line 09 declares a named typed called  errorString  of type  string . Then on line 12, the  error  interface is implemented for the named type. To simulate the  New  function from the  errors  package, a function called  New  is implemented on line 17.

Then on lines 21 and 22, two error variables are declared and initialized. The  ErrNamedType  variable is initialized using the  New  function and the  ErrStructType  is initialized using the  errors.New  function. Finally in  main() , the variables are compared to new values created by the same functions.

When you run the program, the output is interesting. The  if  statement on line 25 is  true  and the  if  statement on line 29 is  false . By using the named type, we are able to create new interface values of type  error  with the same error message and they match. This poses the same problem as in listing 1.12. We could create our own versions of the  error  values and use them. If at any time the package author changes the messages, our code will break.

The same problem can occur when  errorString  is a struct type. Look at what happens when a value receiver is used for the implementation of the  error  interface:

  Listing 1.16  
http://play.golang.org/p/EMWPT-tWp4

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

In listing 1.16 we have implemented the  errorString  struct type using a value receiver for the implementation of the  error  interface. This time we experience the same behavior as we did in listing 1.15 with the named type. When interface type values are compared, the values of the concrete type are compared underneath.

By the standard library using a pointer receiver for the implementation of the  error  interface for the  errorString struct, the  errors.New  function is forced to return a pointer value. This pointer is what is bound to the interface value and will be unique every time. In these cases, pointer values are being compared and not the actual error messages.

Conclusion
In this post we created a foundation for understanding what the  error  interface is and how it is used in conjunction with the  errorString  struct. Using the  errors.New  and  fmt.Errorf  functions for creating interface values of type  error  is a very common practice in Go and highly recommended. Usually a simple string based error message with some basic formatting is all we need to handle errors. 

We also explored a pattern that is used by the standard library to help us identify the different errors that are returned by API calls. Many packages in the standard library create these exported error variables which usually provide enough granularity to identify one particular error over the other. 

There are times when creating your own custom error types make sense. This is something we will explore in part II of this post. For now, use the support provided to us by the standard library for handling errors and follow its example.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值