How does Go Error nesting work?
The design philosophy of Go Error is Errors Are Values.
How to understand this sentence? It’s hard to translate. But from a source perspective, it seems easier to understand what’s behind it.
Go Error source is very simple, a few lines:
// src/builtin/builtin.go
type error interface {
Error() string
}
Copy the code
Error is an interface type, and you only need to implement the error () method. In the Error() method, you can return any content of a custom structure.
Let’s start with how to create an error.
To create the Error
There are two ways to create an error:
errors.New()
;fmt.Errorf()
.
errors.New()
Errors.new () is a continuation of Go, with just a New.
Here’s an example:
package main
import (
"errors"
"fmt"
)
func main(a) {
err := errors.New("This is an error created by errors.new ()")
fmt.Printf("Err error type: %T, error: %v\n", err, err)
}
ErrorString: this is an error created by errors.new () */
Copy the code
The only thing confusing about this code might be the error type, but that’s okay. Just look at the source code, instantly solved.
The source code is as follows:
// src/errors/errors.go
// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
return &errorString{text}
}
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error(a) string {
return e.s
}
Copy the code
As you can see, errorString is a structure that implements the Error() method, and the New function returns the errorString pointer directly.
This usage is simple, but not practical. If I also want to return context information for the program, it won’t do anything.
Now let’s look at the second way.
fmt.Errorf()
Let’s start with an example:
package main
import (
"database/sql"
"fmt"
)
func foo(a) error {
return sql.ErrNoRows
}
func bar(a) error {
return foo()
}
func main(a) {
err := bar()
if err == sql.ErrNoRows {
fmt.Printf("data not found, %+v\n", err)
return
}
iferr ! =nil {
fmt.Println("Unknown error")}}SQL: no rows in result set */
Copy the code
This example produces what we want, but it’s not enough.
In general, we make the return more explicit and more flexible by using the fmt.errorf () function to attach the text information we want to add.
So, foo() would look like this:
func foo(a) error {
return fmt.Errorf("foo err, %v", sql.ErrNoRows)
}
Copy the code
Errorf() encapsulates the original error type and causes err == sql.ErrNoRows to become Unknown error.
If you want to do different processing based on the error type returned, you can’t do that.
So Go 1.13 provides us with wrapError to handle this problem.
Wrap Error
Look at an example:
package main
import (
"fmt"
)
type myError struct{}
func (e myError) Error(a) string {
return "Error happended"
}
func main(a) {
e1 := myError{}
e2 := fmt.Errorf("E2: %w", e1)
e3 := fmt.Errorf("E3: %w", e2)
fmt.Println(e2)
fmt.Println(e3)
}
/* output
E2: Error happended
E3: E2: Error happended
*/
Copy the code
At first glance, it may seem like the same thing, but the underlying principle is different.
Go extends the fmt.errorf () function, adding a %w identifier to create wrapError.
// src/fmt/errors.go
func Errorf(format string, a ...interface{}) error {
p := newPrinter()
p.wrapErrs = true
p.doPrintf(format, a)
s := string(p.buf)
var err error
if p.wrappedErr == nil {
err = errors.New(s)
} else {
err = &wrapError{s, p.wrappedErr}
}
p.free()
return err
}
Copy the code
When w% is used, the function returns &wrapError{s, p.wrapderr}. The wrapError structure is defined as follows:
// src/fmt/errors.go
type wrapError struct {
msg string
err error
}
func (e *wrapError) Error(a) string {
return e.msg
}
func (e *wrapError) Unwrap(a) error {
return e.err
}
Copy the code
The Error() method is implemented, indicating that it is an Error, and the Unwrap() method is used to retrieve the wrapped Error.
// src/errors/wrap.go
func Unwrap(err error) error {
u, ok := err.(interface {
Unwrap() error
})
if! ok {return nil
}
return u.Unwrap()
}
Copy the code
The relationship between them looks like this:
Therefore, we can modify the program above with w% to make it richer in content output.
As follows:
package main
import (
"database/sql"
"errors"
"fmt"
)
func bar(a) error {
iferr := foo(); err ! =nil {
return fmt.Errorf("bar failed: %w", foo())
}
return nil
}
func foo(a) error {
return fmt.Errorf("foo failed: %w", sql.ErrNoRows)
}
func main(a) {
err := bar()
if errors.Is(err, sql.ErrNoRows) {
fmt.Printf("data not found, %+v\n", err)
return
}
iferr ! =nil {
fmt.Println("Unknown error")}}/* output data not found, bar failed: foo failed: sql: no rows in result set */
Copy the code
Finally, the output is satisfactory, with each function adding the necessary context information and conforming to the error type.
The errors.is () function Is used to determine whether err and its encapsulated error chain contain the target type. This solves the problem of not being able to judge the type of error mentioned above.
Afterword.
In fact, Go’s current handling of Error is also controversial. However, the official team is actively communicating with the community to suggest ways to improve. We believe that a better solution will be found in the near future.
At this point, most teams will probably opt for the github.com/pkg/errors package for error handling. If you’re interested, you can learn.
Well, that’s all for this article. Follow me, take you through the problem read Go source.
Source code address:
- Github.com/yongxinz/go…
Recommended reading:
- Why avoid ioutil.readall in Go?
- How to convert []byte to io.reader in Go?
- Start reading Go source code
Reference article:
- Chasecs. Making. IO/posts/the – p…
- medium.com/@dche423/go…
- www.flysnow.org/2019/09/06/…