In background development, error handling is a problem that every programmer will encounter. The error handling package provided by Golang is not so smart and easy to use, so how to gracefully handle and record error information in the code? This article will explore error handling from the following perspectives.

The error of golang

Error in Golang is just a simple interface, and any struct that implements the error () method can be used to handle error messages.

// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
	Error() string
}
Copy the code

Suppose a programmer is developing in the Model layer and a function simply asks for data from a database. He might write the following code

func SomeFunc(id int) (Result, error) {
	result, err := Find(id)
	iferr ! =nil {
                // No data was found. Write a message to the log for future problem location
		log.Info("some err in Find... , err: %v", err)
                // Then return the error
		return Result{}, err
	}
	return result, nil
}
Copy the code

Then, when the next programmer is developing at the Service layer, some function does nothing but call SomeFun() and process Result, so he might write the following code

func ServiceFun(id int) bool {
	result, err := SomeFunc(id)
	iferr ! =nil {
                // SomeFun execute error, write a message to log, convenient for later fault location
		log.Info("some err in SomeFun... , err: %v", err)
		return false
	}
	// do sth to handle result
}
Copy the code

Now there will be an obvious problem, the same error, but printed in the log twice, some people say printed twice, no matter, so that you can easily locate the problem quickly. This is true, but in a large back end system, if a lot of code is logged this way, the log files will be large and much of the information will be repeated, which does not meet the title of this article – gracefully handling errors.

Log.info (” Some err in Find… , err: %v”, err) can be removed, directly return err, but golang error is not call stack, if SomeFun function appears multiple sentences return Result{}, err statement, in the location of the problem is more difficult.

Use github.com/pkg/error to handle errors

Use this library to easily print out the program call stack. Let’s start with a program

func foo(a) error {
	return errors.Wrap(sql.ErrNoRows, "foo failed")}func bar(a) error {
	err := foo()
	return errors.WithMessage(err, "bar failed")}func baz(a) error {
	err := bar()
	return errors.WithMessage(err, "baz failed")
}

fun main() {
    err := baz()
    fmt.Printf("data not found, %v\n", err)    / / tag (1)
    fmt.Printf("data not found, %+v\n", err)   / / / tag (2)
}
Copy the code

Take a look at the output

### The following line is the print of mark ①
data not found, baz failed: bar failed: foo failed: sql: no rows in result set

### The following content is marked ② print content
sql: no rows in result set
foo failed
main.foo
        /path/to/main.go:24
main.bar
        /path/to/main.go:30
main.baz
        /path/to/main.go:35
main.main
        /path/to/main.go:65
runtime.main
        /usr/local/ Cellar/[email protected]/1.15.11 / libexec/SRC/runtime/proc. Go: 204 runtime. Goexit/usr /local/ Cellar/[email protected]/1.15.11 / libexec/SRC/runtime/asm_amd64. S: 1374 bar failed baz failedCopy the code

%v can print all user-defined error messages in the call chain. %+ V can print the complete call stack. This is mainly due to the Wrap function and WithMessage function, let’s look at the source code

// Wrapf returns an error annotating err with a stack trace
// at the point Wrapf is called, and the format specifier.
// If err is nil, Wrapf returns nil.
func Wrapf(err error, format string, args ...interface{}) error {
	if err == nil {
		return nil
	}
	err = &withMessage{
		cause: err,
		msg:   fmt.Sprintf(format, args...),
	}
	return &withStack{
		err,
		callers(),
	}
}

// WithMessage annotates err with a new message.
// If err is nil, WithMessage returns nil.
func WithMessage(err error, message string) error {
	if err == nil {
		return nil
	}
	return &withMessage{
		cause: err,
		msg:   message,
	}
}

type withMessage struct {
	cause error
	msg   string
}

type withStack struct {
	error
	*stack
}
Copy the code

The Wrap function returns an error marked with an error stack. The WithMessage function returns an error marked with a message. The WithMessage return is a WithMessage structure, and the withStack structure contains a *stack, which is returned by callers(), and contains the error stack information.

Back in main, we can expand err := baz() to take a closer look at its structure

It is now easy to understand the output of %v and %+v.

Note that the Wrap function is best used at the bottom of the call and should be used only once. The error of other layers can be simply added with WithMessage to add context information. If Wrap is used for each layer, it will cause each Wrap to appear. If the log is printed in %+v mode at the top level, one more call stack will be printed. Unless these stacks are necessary information, a large number of logs will accumulate, which will cause inconvenience for maintenance.

further

Although it is convenient to see a detailed call stack for error location, too much stack information printing can still be a great burden on the logging system, sometimes the problem location often does not require a detailed call stack, just a call chain, for example

main.mian()@line: err ==> main.a()@line: err ==> b.b()@line: err ==> c.c()@line: err ... ==> fun()@line: some err

We can then Wrap github.com/pkg/error with the name of the function and the number of lines each time we call Wrap and WithMessage

package MyError

import (
	"github.com/pkg/errors"
	"runtime"
	"strconv"
)

/** * @author: chapaofan * @date: 2021/5/152:08 PM */

func Wrap(err error, message string) error {
	return errors.Wrap(err, "= = >"+printCallerNameAndLine()+message)
}

func WithMessage(err error, message string) error {
	return errors.WithMessage(err, "= = >"+printCallerNameAndLine()+message)
}

func printCallerNameAndLine(a) string {
	pc, _, line, _ := runtime.Caller(2)
	return runtime.FuncForPC(pc).Name() + "@" () + strconv.Itoa(line) + ":"
}

Copy the code

To execute the main function at this point, you can get the complete call chain, and avoid a complicated call stack

data not found, ==> main.baz()@39: baz failed: ==> main.bar()@33: bar failed: ==> main.foo()@24: foo failed: sql: no rows in result set
Copy the code