This paper analyzes the evolution of error and the best practice, so as to have an overall understanding of error and some problems in the use of error in the standard library.

Article directory structure

The evolution of Error1.13Previous error PKG /errors1.13The error PKG/errors1.13 error
   2.0Error suggests obtaining the Error best practices for the Panic call stackCopy the code

The evolution of Error

The error implementation prior to 1.13 was very simple, essentially a type error interface {error () string}, The error created by the New(), FMT.Errorf() method is a type of errorString, which simply nested a string field. Because of the simple function, some problems will be encountered in the actual use.

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

func (e *errorString) Error(a) string {
    return e.s
}
Copy the code

First of all, when troubleshooting problems, you can only increase the description information, layer upon layer printing, will cause a service online, it is possible to hit dozens of error logs, these logs scattered in the system, when the query to associate the whole context is very troublesome:

func main(a) {
    err := test1()
    iferr ! =nil {
        log.Println("test1 error", err)
        return}}func test1(a) error {
    err := test2()
    iferr ! =nil {
        log.Println("test2 error", err)
        return err
    }
    return nil
}

func test2(a) error {
    _, err := test3()
    iferr ! =nil {
        log.Println("test2 error", err)
        return err
    }
    return nil
}

func test3(a) (string, error) {
    m := make(map[string]string)
    b, err := json.Marshal(m)
    iferr ! =nil {
        log.Println("test3 error", err)
        return "", err
    }
    return string(b), err
}
Copy the code

Errorf(“more info: %v”, err) can be wrapped to the top layer to print the log, but this introduces two problems, so it is not recommended to use this wrapper to handle error: 1. The loss of root causes, such as sentinel errors, if they are wrapped to make one error, in which case we can tell by == that two errors are no longer valid, The new error can only be determined by string matching, which is not elegant in implementation. FMT.Errorf(“more info: %v”, err), so it becomes an errorString error. We want to type assert this error to be a custom error and it will always be false.

func test3(a) error {
    return fmt.Errorf("test3 err, %v", sql.ErrNoRows)
}

func test2(a) error {
    return sql.ErrNoRows
}

func test1(a) error {
    return test2()
}

func main(a) {
    err := test1()
    iferr ! =nil {
        if err == sql.ErrNoRows { // In normal cases, you can do the equivalent judgment processing directly
            fmt.Printf("data not found, %+v\n", err)
        }
    }

    err = test3()
    iferr ! =nil {
        if strings.Contains(err.Error(), sql.ErrNoRows.Error()) { // After wrapping, only string matching can be used to determine equality
            fmt.Printf("data not found, %+v\n", err)
        }
    }
}
Copy the code

Finally, we can’t log the stack of program calls through error, which is very unfriendly for troubleshooting.

pkg/errors

Because of the above slots in Go Error, many error handling libraries were born before Go1.13 and the function of Wrap was added. Among them, github.com/pkg/errors, which was open source in 2016, is a relatively simple one with very powerful functions. PKG/Errors as an example, here is a solution to the problem of error before 1.13. The first problem is the layer-by-layer printing of the log and the call stack recording. We can hold the stack information at the source of the error by means of errors.WithStack\errors.New\ errors.Errorf\errors.Wrap\errors.Wrapf, etc. The upper layer then calls errors.WithMessage to wrap the error and return to the upper layer. Finally, it is ok to print the log in the program entrance. Using %+v, you can print the error information and stack information of the whole link:

func main(a) {
    err := test1()
    iferr ! =nil {
        log.Println(err)
        // log.printf ("%+v", err) prints call stack information
        return}}func test1(a) error {
    err := test2()
    iferr ! =nil {
        return errors.WithMessage(err, "test1 error") // the middle layer returns an error wrapped WithMessage
    }
    return nil
}

func test2(a) error {
    err := test3()
    iferr ! =nil {
        return errors.WithMessage(err, "test2 error") // the middle layer returns an error wrapped WithMessage
    }
    return nil
}

WithStck, errors.New, errors.Errorf, errors.Wrap save the call stack
func test3(a) error {
    _, err := os.Open("no exist")
    iferr ! =nil {
        // Method 1: Errors. WithStack saves call stack information
        return errors.WithStack(err)
    }

    // Method 2: Errors. New saves call stack information
    // return errors.New("test3 errors happens")

    Errorf is used to save the call stack information
    // return errors.Errorf("test3 errors happens, id: %v", id)

    // Method 4: Errors. Wrap saves call stack information
    // return errors.Wrap(err, "test3 error")

    return err
}}

// Prints the result: different errors are separated by: with hierarchy
test1 error: test2 error: open no exist: no such file or directory
Copy the code

We can now use the errors.Cause method to determine whether err is a predefined error:

func test3(a) error {
    return errors.Wrap(sql.ErrNoRows, "test3 err")}func test2(a) error {
    err := test3()
    return errors.Wrap(err, "test2 err")}func test1(a) error {
    err := test2()
    return errors.Wrap(err, "test1 err")}func main(a) {
    err := test1()
    iferr ! =nil {
        if errors.Cause(err) == sql.ErrNoRows { // Call Cause to get the original error
            fmt.Printf("data not found, %v\n", err)
        }
    }
}

// Print output:
data not found, test1 err: test2 err: test3 err: sql: no rows in result set
Copy the code

The last one is for the custom error, we can not restore the error type after wrapping error before, now we can restore the custom error through Cause method to determine the assertion:

type customError struct {
    s string
}

func (ce *customError) Error(a) string {
    return ce.s
}

func main(a) {
    _, err := openFile()
    iferr ! =nil {
        if _, ok := errors.Cause(err).(*customError); ok {
            fmt.Println("err is file error")}}}func openFile(a) ([]byte, error) {
    return nil, errors.WithStack(&customError{"test"})}Copy the code

1.13 the error

In September 2019, Go1.13 was released, with some improvements to the Errors library and the introduction of Wrapping Error (without the Wrap method, Errorf Is directly extended to Wrap Error (%w) and to add Is/As/Unwarp methods for secondary processing and recognition of returned errors. WrapError is a custom wrapError type that wraps an error and a message field. The warped errors form a linked list of errors. Unwarp returns only the outermost layer of errors. If you want to retrieve the inner layer, you need to call errors.Unwarp several times.

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
}

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

Errorf(“some other info, %w”, err); Errorf(“some other info, %w”, err); Errorf(“some other info, %w”, err) The problem of determining the root cause after the error Is wrapped can be realized by using the Is method. The Is method will use the Unwrap method to ‘Unwrap’ the nested error layer by layer, and then compare it with the error to be judged. True if both are equal.

func test3(a) error {
    return fmt.Errorf("test3 err: %w", sql.ErrNoRows)
}

func test2(a) error {
    err := test3()
    return fmt.Errorf("test2 err: %w", err)
}

func test1(a) error {
    err := test2()
    return fmt.Errorf("test1 err: %w", err)
}

func main(a) {
    err := test1()
    iferr ! =nil {
        if errors.Is(err, sql.ErrNoRows) { // Call Is to get whether error Is sql.errnorows
            fmt.Printf("data not found, %v\n", err)
        }
    }
}

// Print output:
data not found, test1 err: test2 err: test3 err: sql: no rows in result set
Copy the code

In addition, to determine whether an error is a custom error, error can not be obtained after wrapping error, now it is very convenient to implement through As method, the key is that there is no need to implement through assertions:

type customError struct {
    s string
}

func (ce *customError) Error(a) string {
    return ce.s
}

func main(a) {
    var cerror *customError
    _, err := openFile()
    iferr ! =nil {
        if errors.As(err, &cerror) {
            fmt.Println("err type is customError")}}}func openFile(a) ([]byte, error) {
    return nil, &customError{"test"}}// Print the result:
err type is customError
Copy the code

PKG /errors fits 1.13 error

In order to adapt to the usage in 1.13 error and unify the code style, the PKG /error library implements the Unwrap method on the withStack, withMessage and other error structures, and introduces the Is/As method:

import (
    stderrors "errors"
)
func Is(err, target error) bool { return stderrors.Is(err, target) }
func As(err error, target interface{}) bool { return stderrors.As(err, target) }
func Unwrap(err error) error {
    return stderrors.Unwrap(err)
}
Copy the code

In this way, after using PKG /error to wrap error, we can also use the usage in 1.13 to restore error and determine the type, without calling Cause method to determine:

import (
    "fmt"

    "github.com/pkg/errors"
)

func test3(a) error {
    return errors.Wrap(sql.ErrNoRows, "test3 err")}func test2(a) error {
    err := test3()
    return errors.Wrap(err, "test2 err")}func test1(a) error {
    err := test2()
    return errors.Wrap(err, "test1 err")}func main(a) {
    err := test1()
    iferr ! =nil {
        if errors.Is(err, sql.ErrNoRows) { // Call the Cause method without calling the Is method
            fmt.Printf("data not found, %v\n", err)
        }
    }
}
Copy the code

As method can also be called to determine whether the error type is a custom error, without predicate judgment after obtaining the original error using Cause before:

import (
    "fmt"

    "github.com/pkg/errors"
)

type customError struct {
    s string
}

func (ce *customError) Error(a) string {
    return ce.s
}

func main(a) {
    var cerror *customError
    _, err := openFile()
    iferr ! =nil {
        if errors.As(err, &cerror) {
            fmt.Println("err is customError")}}}func openFile(a) ([]byte, error) {
    return nil, errors.WithStack(&customError{"test"})}Copy the code

The “%w” placeholder is less concise than the warp method, and PKG /errors is designed to match the 1.13 error. So in terms of overall practicality, I still recommend using PKG /error to handle errors.

2.0 the error proposed

In engineering practice, error is the most ridicule should be if err! = nil. If the logic is complex, there will be a lot of error handling in the code, which can be very lengthy. For example, in the following example, there are only 4 lines of function calls, but the code handling error is 12 lines.

 x, err := test1()
    iferr ! =nil {
        // handle error
    }
    y, err := test2()
    iferr ! =nil {
        // handle error
    }
    z, err := test3()
    iferr ! =nil {
        // handle error
    }
    s, err := test4()
    iferr ! =nil {
        // handle error}...Copy the code

So Error Handling is proposed as a major change in the Golang 2 proposal: Two keywords, check and handler, are added to handle errors uniformly. Check is used to display and mark errors, and handle is used to define the error processing logic. Once the check reaches the specified error, the corresponding error processing logic will be entered. Check and handler make the overall code logic more concise, and errors can be handled uniformly at handle, similar to try/catch:

func CopyFile(src, dst string) error {
        handle err {
                return fmt.Errorf("copy %s %s: %v", src, dst, err)
        }

        r := check os.Open(src)
        defer r.Close()

        w := check os.Create(dst)
        handle err {
                w.Close()
                os.Remove(dst) // (only if a check fails)
        }

        check io.Copy(w, r)
        check w.Close()
        return nil
}
Copy the code

Get the Panic call stack

The exception we caught through the recover function is an interface. We can determine whether the interface is nil to determine whether panic has occurred in the program and catch panic to prevent the program from exiting, but only panic information is not enough. We need to locate which line of code has the problem through the recovered log.

func main(a) {
    defer func(a) {
        if err := recover(a); err ! =nil {
            fmt.Println(err)
        }
    }()

    test1()
    test2()
}

func test1(a) {
    var m map[string]int
    m["1"] = 1
}
func test2(a) {
    var m map[string]int
    m["2"] = 1
}
Copy the code

Therefore, when recovering the log, you generally need to manually bring the function call Stack information with debug.stack (), or directly call debug.printstack () to print out the call Stack information of the function defer.

func main(a) {
    defer func(a) {
        if err := recover(a); err ! =nil {
            fmt.Printf("err: %v, catch panic: %s", err, debug.Stack()) // Debug.Stack() can be used to capture function Stack information
        }
    }()

    test1()
    test2()
}

func test1(a) {
    var m map[string]int
    m["1"] = 1
}
func test2(a) {
    var m map[string]int
    m["2"] = 1
}

Copy the code

Error best practices

Through the above analysis, we still recommend the PKG/ERROR method to deal with error, so the best practices here are mainly for some experience summary of business code on PKG /error full link transformation: First: At the source of the error, the stack is reserved for database calls, RPC calls, or rule validation, etc. The New/Errorf of PKG /errors can be returned using the library errors before, and the current stack context has been reserved. Second: if you call a return from another library in your own business infrastructure, you will not process direct pass-through twice, but directly throw up. For example, if you call the bPackage method, it returns an error, which is thrown directly up without a WithStack wrapper. Because the first person who did the WithStack or Errorf/New package has already saved the stack, there’s no need to save it a second time, so I exit the method that comes back from the same package because it might have been processed, Call the errors.withMessage method if you need to add some additional context information. The third: When we interact with Go’s standard library or third-party library, we need WithStack to record the error. I know that the third-party library reported an error somewhere. However, for the standard library returned, such as SQL. ErrNoRows, it is recommended not to package, if the package is broken, it will break the previous business code. It will lead to untenable judgment, because we cannot require all businesses to use Cause/Is method to restore the root Cause and then judge, which will destroy the previous ones. Fourth: type the log at the top, not everywhere, it is best to package the error print in a unified middleware to print. Reference: www.sohu.com/a/342949702…