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
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
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 theerr 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 anderrorString 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
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 astring. 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 aboutInterfaces, Methods and Embedded Types.
The standard library has also declared a struct type called errorString that can be found in the errorspackage:
Listing 1.4
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
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 typeerrorString 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 errorwithin 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 theerrors package is the way to go:
Listing 1.6
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 theNew function:
Listing 1.7
// 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
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
// 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 fmtpackage to create your interface value of type error, the value underneath is always a pointer of typeerrorString.
Now we know the two different ways we can create interface values of type error using a pointer of typeerrorString. 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 errorspackage to create package level error variables:
Listing 1.10
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 bufiopackage API’s:
Listing 1.11
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. ThePeek 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
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
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
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
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
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 errorStringstruct, 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.
有疑问加站长微信联系(非本文作者)