Those of you who have written C know that the C language often returns an integer error code (errno) to indicate that the function has gone wrong, usually -1 for error and 0 for correct.
In Go, we use the error type to indicate an error, but it is no longer an integer, but an interface type:
type error interface {
Error() string
}
Copy the code
It represents errors that can be explained with a single string.
The errors.new () function we use most often is errors.new (), which is very simple:
// src/errors/errors.go
func New(text string) error {
return &errorString{text}
}
type errorString struct {
s string
}
func (e *errorString) Error(a) string {
return e.s
}
Copy the code
The error type created using the New function is actually the unexported errorString from the Errors package. It contains a unique field S and implements a unique method: error () String.
Usually this is enough and it shows that something “went wrong” at the time, but sometimes we need more specific information, such as:
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("math: square root of negative number")}// implementation
}
Copy the code
When the caller finds an error, it only knows that a negative number has been passed in, but it is not clear what value has been passed in. In the Go:
It is the error implementation’s responsibility to summarize the context.
It requires the function that returns the error to give specific “context” information, that is, what the negative number is in the Sqrt function.
So, if you find that f is less than 0, you should return an error like this:
if f < 0 {
return 0, fmt.Errorf("math: square root of negative number %g", f)
}
Copy the code
This uses the fmt.Errorf function, which formats the string and then calls errors.New to create the error.
When we want to know the type of error and print an error, we print error:
fmt.Println(err)
Copy the code
Or:
fmt.Println(err.Error)
Copy the code
The FMT package automatically calls the err.error () function to print the string.
Usually, we put error at the end of the return value of the function, there’s nothing to talk about, everybody does it, it’s a convention.
Reference [Tony Bai] mentioned in this article that when constructing an error, it is required that the string passed in should begin with a lowercase letter and end with no punctuation marks. This is because we often use the returned error as follows:
. err := errors.New("error example")
fmt.Printf("The returned error is %s.\n", err)
Copy the code
The error of the dilemma
In Go, error handling is important. The language’s design and conventions encourage you to explicitly check for errors where they occur (as distinct from the convention in other languages of throwing exceptions and sometimes catching them).
Error handling is very important in the Go language. It requires that we explicitly handle the errors we encounter at the linguistic level. Instead of using try-catch-finally “tricks” like other languages like Java.
This causes errors to fly through the code, making it very long and slow.
For the sake of code robustness, we should not ignore every error returned by a function. Because if something goes wrong, it might return an object of type nil. If the error is not judged, the next line of operations on nil will 100% cause a panic.
Thus, one of the biggest complaints about Go is that its error handling seems to hark back to the days of ancient C.
rr := doStuff1()
iferr ! =nil {
//handle error...
}
err = doStuff2()
iferr ! =nil {
//handle error...
}
err = doStuff3()
iferr ! =nil {
//handle error...
}
Copy the code
Russ Cox, one of Go Authors, refutes this argument by saying that the error handling mechanism of return values was chosen over try-catch because the former was suitable for large software and the latter was more suitable for small programs.
As mentioned in the Go FAQ, try-catch causes code to get messy. Programmers tend to throw common errors, such as failing to open a file, into exceptions, which makes error handling more verbose and error-prone.
The multiple return values of the Go language make it surprisingly easy to return errors. For true exceptions, Go provides a panic-recover mechanism, which also makes the code look very clean.
Of course, Russ Cox admits that Go’s error-handling mechanism does impose a certain mental burden on developers.
Is Go’s error handling a good design? The former uses error, while the latter uses panic, which is more advantageous than Java’s one-size-fits all approach to handling errors and exceptions.
There are some good examples of how to handle errors in a business context.
Try to break the bureau
This section is based on the Dave Cheney GoCon 2016 talk, and the references are directly available.
It’s often heard that Go has a lot of “quotes” that come off the tongue, but it’s not always easy to understand because most of them have a story. For example, we often say:
Don’t communicating by sharing memory, share memory by communicating.
The article also lists many, all very interesting:
Here are three “proverbs” about error.
Errors are just values
Errors are just values means that any type of interface that implements Error can be considered an Error. It is important to understand the reasoning behind these aphorisms.
The author divides the handling of error into three ways:
- Sentinel errors
- Error Types
- Opaque errors
Let’s take them one by one. First of all, Sentinel errors, Sentinel comes from a commonly used word in computers, which means “Sentinel” in Chinese. In the old days, when you were learning quicksort, there was a sentinel, and everything else was compared to the sentinel, and it drew a line.
What Sentinel errors really means here is that there is an error that indicates that the process can’t go any further, it has to stop there, and that’s a boundary. And these mistakes are often agreed in advance.
For example, IO.EOF in the IO package means “end of file” error. But this approach is not very flexible:
func main(a) {
r := bytes.NewReader([]byte("0123456789"))
_, err := r.Read(make([]byte.10))
if err == io.EOF {
log.Fatal("read failed:", err)
}
}
Copy the code
Check whether err is equal to io.eof.
Here’s another example. I get in trouble when I want to return err and add some context:
Func main() {err := readfile(".bashrc ") if strings.Contains(err.error (), "not found") { // handle error } } func readfile(path string) error { err := openfile(path) if err ! Errorf(" Cannot open file: %v", err)} //... }Copy the code
Errorf is used to add the file information before err and return it to the caller. The err returned is actually a string.
As a result, the caller has to use string matching to determine whether the underlying function readfile has some kind of error. When you have to do this to identify certain errors, the “bad smell” of code comes into play.
By the way, the err.error () method is designed for programmers, not code. That is, when we call the Error method, the results are written to a file or printed out for the programmer to see. In code, we can’t make any decisions based on err.error (), as we did in the main function above. Bad.
The biggest problem with Sentinel Errors is that it creates a dependency between defining an error and the package that uses it. Err == IO.EOF, for example, requires the IO package, which is the standard library package. If a lot of user-defined packages define errors, THEN I have to introduce a lot of packages to determine the various errors. The trouble is, this tends to cause circular reference problems.
Therefore, Sentinel errors should be avoided as much as possible, although there are some packages in the standard library that do this, but it is not recommended to emulate them.
The second is Error Types, which refer to those Types that implement the Error interface. An important benefit of this is that you can have other fields in a type besides error to provide additional information, such as the number of lines in error.
The standard library has a very good example:
// PathError records an error and the operation and file path that caused it.
type PathError struct {
Op string
Path string
Err error
}
Copy the code
PathError additionally records the file path and operation type when the error occurred.
In general, with an error type like this, the outer caller needs to use a type assertion to determine the error:
// underlyingError returns the underlying error for known os error types.
func underlyingError(err error) error {
switch err := err.(type) {
case *PathError:
return err.Err
case *LinkError:
return err.Err
case *SyscallError:
return err.Err
}
return err
}
Copy the code
But this inevitably creates a dependency between defining the wrong package and using the wrong package, which brings us back to the previous problem.
Even though Error types are better than Sentinel Errors because it can carry more context information, it still introduces the problem of package dependencies. Therefore, it is not recommended. At the very least, do not include Error types as an exported type.
And finally, Opaque errors. Translation: “black box errors”, because you can know that an error happened, but you can’t see what’s inside it.
For example, the following pseudocode:
func fn(a) error {
x, err := bar.Foo()
iferr ! =nil {
return err
}
// use x
return nil
}
Copy the code
As a caller, all you need to know after calling Foo is whether Foo worked or something went wrong. That is, you just need to check if err is empty, and if it is not, you return an error. Otherwise, proceed with the normal process without knowing what err is.
That’s the strategy for dealing with Opaque errors.
Of course, in some cases, this may not be enough. For example, in a network request, the caller needs to determine the type of error returned in order to decide whether to retry. In this case, the author offers a method:
In this case rather than asserting the error is a specific type or value, we can assert that the error implements a particular behaviour.
That is, instead of determining what the type of error is, determine whether the error has some behavior or implements some interface.
Here’s an example:
type temporary interface {
Temporary() bool
}
func IsTemporary(err error) bool {
te, ok := err.(temporary)
return ok && te.Temporary()
}
Copy the code
When the network request returns an error, call the istemtempo () function again.
The advantage of this is that there is no need to import references to misdefined packages in the network request package.
handle not just check errors
The second maxim in this section is, “Don’t just check errors, handle them gracefully.”
func AuthenticateRequest(r *Request) error {
err := authenticate(r.User)
iferr ! =nil {
return err
}
return nil
}
Copy the code
The code in the above example has a problem and can be optimized into a sentence:
func AuthenticateRequest(r *Request) error {
return authenticate(r.User)
}
Copy the code
There are other problems. At the very top of the function call chain, we might get an error: No such file or directory.
This error gives too little information about the file name, path, line number, etc.
Try to improve it by adding a little context:
func AuthenticateRequest(r *Request) error {
err := authenticate(r.User)
iferr ! =nil {
return fmt.Errorf("authenticate failed: %v", err)
}
return nil
}
Copy the code
This is essentially an error converted to a string, concatenated to another string, and finally converted to an error via FMT.Errorf. This breaks the equality detection, meaning we can’t tell if an error is a predefined error.
The solution is to use a third-party library: github.com/pkg/errors. Provides a friendly interface:
// Wrap annotates cause with a message.
func Wrap(cause error, message string) error
// Cause unwraps an annotated error.
func Cause(err error) error
Copy the code
With Wrap, you can “Wrap” an error into a new error by adding a string. Through Cause, the opposite operation can be carried out to restore the error in the inner layer.
With these two functions, it is much more convenient:
func ReadFile(path string) ([]byte, error) {
f, err := os.Open(path)
iferr ! =nil {
return nil, errors.Wrap(err, "open failed")}defer f.Close()
buf, err := ioutil.ReadAll(f)
iferr ! =nil {
return nil, errors.Wrap(err, "read failed")}return buf, nil
}
Copy the code
This is a function that reads a file. It tries to open the file first, and returns an error message with “Open Failed” appended. After that, you try to read the file, and if something goes wrong, you return an error with “read failed” attached.
When the ReadFile function is called from the outer layer:
func main(a) {
_, err := ReadConfig()
iferr ! =nil {
fmt.Println(err)
os.Exit(1)}}func ReadConfig(a) ([]byte, error) {
home := os.Getenv("HOME")
config, err := ReadFile(filepath.Join(home, ".settings.xml"))
return config, errors.Wrap(err, "could not read config")}Copy the code
In main we can print an error message like this:
could not read config: open failed: open /Users/dfc/.settings.xml: no such file or directory
Copy the code
It’s hierarchical, it’s very clear. If we use the printout provided by the PKG/Errors library:
func main(a) {
_, err := ReadConfig()
iferr ! =nil {
errors.Print(err)
os.Exit(1)}}Copy the code
You can get more hierarchical and detailed errors:
readfile.go:27: could not read config
readfile.go:14: open failed
open /Users/dfc/.settings.xml: no such file or directory
Copy the code
Let’s look at the “Cause” function, using the temporary interface as an example:
type temporary interface {
Temporary() bool
}
// IsTemporary returns true if err is temporary.
func IsTemporary(err error) bool {
te, ok := errors.Cause(err).(temporary)
return ok && te.Temporary()
}
Copy the code
Cause is used to fetch errors, assertions are made, and Temporary is recursively called. If the error does not implement the TEMPORARY interface, the assertion fails and false is returned.
Only handle errors once
What does “handle” an error mean?
Handling an error means inspecting the error value, and making a decision.
It means to look at the mistakes and make a decision.
For example, to make no decision is to ignore the error:
func Write(w io.Writer, buf []byte) {
w.Write(buf)
w.Write(buf)
}
Copy the code
W.write (buf) returns two results, one for the number of bytes successfully written and one for error, which the above example did nothing to.
The following example handles two more errors:
func Write(w io.Writer, buf []byte) error {
_, err := w.Write(buf)
iferr ! =nil {
// annotated error goes to log file
log.Println("unable to write:", err)
// unannotated error returned to caller
return err
return err
}
return nil
}
Copy the code
The first is to log the error, and the second is to return the error to the upper-level caller. The caller may log the error or return it to the upper layer.
As a result, the log file is filled with repeated error descriptions, and to the uppermost caller (for example, the main function), it still gets the error returned by the lowest function, without any context information.
Error packages from third parties can be used to solve the problem perfectly:
func Write(w io.Write, buf []byte) error {
_, err := w.Write(buf)
return errors.Wrap(err, "write failed")}Copy the code
The error returned is friendly to both humans and machines.
summary
This section focuses on the principles for handling errors and introduces a third-party errors package to make error handling more elegant.
The author concludes with some conclusions:
- Errors, like externally provided apis, need to be taken seriously.
- Think of Errors as a black box and judge its behavior, not its type.
- Try not to use Sentinel Errors.
- Wrap error (errors.wrap) with a third-party error package to make it more usable.
- Use errors.Cause to retrieve the underlying error.
The stillborn try proposal
Previous proposals to improve error handling with the “check & Handle” keyword and the “try built-in function” have been officially rejected in advance due to overwhelming community opposition.
For details on these two proposals, see Resources check & Handle and Try Proposals.
Improvements to Go 1.13
Some failed attempts at the Go language, such as the introduction of Vendor and internal to manage packages in Go 1.5, ended up being abused and caused a lot of problems. So Go 1.13 directly ditches the GOPATH and Vendor features and manages packages with Modules instead.
In the article “Go Language has been established for 10 years, Go2 Is Ready for Development”, Chai Da said:
For example, a recent proposal by Robert Griesemer, one of the fathers of the Go language, to simplify error handling by using the try built-in function was rejected. The failure to do so is a good sign that Go is still being tested in some new areas — the language is still alive and well.
On September 3, Go released version 1.13, which improved numeric literals in addition to the Module feature. Also important is defer, which improves performance by 30%, moves more objects from the heap onto the stack to improve performance, and so on.
Another major improvement occurs in the Errors library. The Errors library adds the Is/As/Unwrap functions, which will support error repackaging and recognition handling in advance of the new error handling improvements in Go 2.
1.13 supports error wrapping:
An error e can wrap another error w by providing an Unwrap method that returns w. Both e and w are available to programs, allowing e to provide additional context to w or to reinterpret it while still allowing programs to make decisions based on w.
To support wrapping, FMT.Errorf adds a %w format and three functions in the error package: errors.Unwrap, errors.Is, errors.As.
fmt.Errorf
Errorf plus the %w formatter generates a nested error. It does not use a Wrap function to nest errors like PKG /errors, which is very concise.
Unwrap
func Unwrap(err error) error
Copy the code
Parsing out nested errors. Multiple layers of nesting need to call the Unwrap function many times to get the error of the innermost layer.
The source code is as follows:
func Unwrap(err error) error {
// Determine if the Unwrap method is implemented
u, ok := err.(interface {
Unwrap() error
})
// If not, return nil
if! ok {return nil
}
// Call the Unwrap method to return a nested error
return u.Unwrap()
}
Copy the code
Assert err to see if it implements the Unwrap method, and if so, call its Unwrap method. Otherwise, return nil.
Is
func Is(err, target error) bool
Copy the code
Check whether err is of the same type as target or whether err nested errors are of the same type as target. If so, return true.
The source code is as follows:
func Is(err, target error) bool {
if target == nil {
return err == target
}
isComparable := reflectlite.TypeOf(target).Comparable()
// Infinite loop, comparing err and nested errors
for {
if isComparable && err == target {
return true
}
// Call error's Is method, which can be implemented by custom
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
return true
}
// Returns the nested error of the next layer
if err = Unwrap(err); err == nil {
return false}}}Copy the code
Through an infinite loop, Unwrap the nested error in err to see if the unwrapped error implements the Is method and calls its Is method. When both return true, the whole function returns true.
As
func As(err error, target interface{}) bool
Copy the code
Find the equivalent of target from the err chain and set the variable to which target points.
The source code is as follows:
func As(err error, target interface{}) bool {
// Target cannot be nil
if target == nil {
panic("errors: target cannot be nil")
}
val := reflectlite.ValueOf(target)
typ := val.Type()
// Target must be a non-null pointer
iftyp.Kind() ! = reflectlite.Ptr || val.IsNil() {panic("errors: target must be a non-nil pointer")}// Make sure target is an interface type or implements the Error interface
ife := typ.Elem(); e.Kind() ! = reflectlite.Interface && ! e.Implements(errorType) {panic("errors: *target must be interface or implement error")
}
targetType := typ.Elem()
forerr ! =nil {
// Use reflection to determine if it can be assigned, if so, and return true
if reflectlite.TypeOf(err).AssignableTo(targetType) {
val.Elem().Set(reflectlite.ValueOf(err))
return true
}
// Call the error custom As method to implement your own type assertion code
if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) {
return true
}
// Unwrap the nested error layer by layer
err = Unwrap(err)
}
return false
}
Copy the code
Returns true if an err in the error chain can be assigned to the variable pointed to by target; Or the As(Interface {}) bool method implemented by Err returns true.
The former assigns err to the variable pointed to by target. The latter, provided by the As function.
The function panic occurs if target is not a non-null pointer to a type that implements an error interface or some other interface type.
This part of the content, flying snow ruthless big guy’s article [flying snow ruthless analysis 1.13 error] write better, recommended reading.
conclusion
The use of error and panic to handle errors and exceptions in Go is a very good and clear practice. Whether to use Error or Panic depends on the specific business scenario.
Of course, error in Go is too simple to record much context, and there is no good way to handle error packages. Of course, these can be addressed through third-party libraries. This area has also been improved in the new go 1.13 release and will be further improved in Go 2.
This article also provides examples of handling errors, such as not handling the same error twice, judging the wrong behavior rather than the type, and so on.
Resources provides many examples of error handling, and this article serves as a primer.
The resources
【 Go wrong proposal 2 】 go.googlesource.com/proposal/+/…
Check & handle go.googlesource.com/proposal/+/ 】…
[Issue of wrong discussion] github.com/golang/go/i…
【 Error Value FAQ】github.com/golang/go/w…
[Error package] golang.org/pkg/errors/
Blowing merciless blog error handling 】 【 www.flysnow.org/2019/01/01/…
[blowing merciless analysis error 】 1.13 www.flysnow.org/2019/09/06/…
[Tony Bai Go] tonybai.com/2015/10/30/…
【Go official error using tutorial 】blog.golang.org/error-handl…
【Go FAQ】golang.org/doc/faq#exc…
【 ethancai error handling 】 ethancai. Dead simple. IO / 2017/12/29 /…
】 【 Dave cheney GoCon 2016 speech dave.cheney.net/paste/gocon…
【Morsing’s Blog Effective error handling in Go】morsmachine.dk/error-handl…
【 how elegant in Golang error handling 】 www.ituring.com.cn/article/508…
Go 2 error handling proposal: Try or check? Toutiao. IO/posts/uh9qo…
【 Try proposal 】github.com/golang/go/i…
【 Reject try proposal 】github.com/golang/go/i…
Is Go’s error handling a good design? www.zhihu.com/question/27…