The error handling of Go is often teased by people in daily life. I have also observed some phenomena in my work. The most serious one is that there are some repetitions of error handling in logic codes at all levels.

For example, someone writing code will make mistakes at every level and log them. From the code level, it looks very rigorous, but if you look at the log, you will find a bunch of duplicate information, which will cause interference when troubleshooting problems.

Today I want to summarize three best practices related to error handling in Go code.

These best practices are also shared by some predecessors on the Internet, and I use my own language to describe them here, hoping to help you.

Know the error

The Go program indicates errors by values of type error

The error type is a built-in interface type that only specifies an error method that returns a string value.

type error interface {
    Error() string
}
Copy the code

Go language functions often return an error value, and the caller does error handling by testing if the error value is nil.

i, err := strconv.Atoi("42") if err ! = nil { fmt.Printf("couldn't convert number: %v\n", err) return } fmt.Println("Converted integer:", i)Copy the code

Error is nil, success; Non-nil error means failure.

Remember to implement the error interface

We often define error types that fit our needs, but remember to implement the error interface for these types so that we don’t have to introduce additional types into the caller’s program.

For example, if we define myError, the caller’s code will be invaded by myError if we do not implement the error interface. For example, when defining the return type of the run function, we can simply define it as error.

package myerror import ( "fmt" "time" ) type myError struct { Code int When time.Time What string } func (e *myError) Error() string { return fmt.Sprintf("at %v, %s, code %d", e.When, e.What, e.Code) } func run() error { return &MyError{ 1002, time.Now(), "it didn't work", } } func TryIt() { if err := run(); err ! = nil { fmt.Println(err) } }Copy the code

If myError does not implement the error interface, the return value type is defined as myError. As you can imagine, the next step in the caller’s program is to use myerror. Code == XXX to determine what kind of specific error it is (of course, to do this, you need to change myError to the exported myError).

What should the caller do to determine what kind of error a custom error is? MyError is not exposed outside the package. The answer is to expose methods that check for error behavior outside the package.

myerror.IsXXXError(err) 
...
Copy the code

Or by comparing the error itself to the constant errors exposed by the package, such as io.eof, which is often used to determine whether a file is closed or not.

Similar error constants are exposed by various open source packages such as Gorm.ErrRecordNotFound.

if err ! = io.EOF { return err }Copy the code

A common error in error handling

Let’s look at a simple program, and see if you can spot some subtle problems

func WriteAll(w io.Writer, buf []byte) error { _, err := w.Write(buf) if err ! = nil { log.Println("unable to write:", err) // annotated error goes to log file return err // unannotated error returned to caller } return nil } func WriteConfig(w io.Writer, conf *Config) error { buf, err := json.Marshal(conf) if err ! = nil { log.Printf("could not marshal config: %v", err) return err } if err := WriteAll(w, buf); err ! = nil { log.Println("could not write config: %v", err) return err } return nil } func main() { err := WriteConfig(f, &conf) fmt.Println(err) // io.EOF }Copy the code

There are two common problems with error handling

The error handling of the above program exposes two problems:

  1. When an error occurs, the WriteAll function also logs the error to the upper level. The upper level caller does the same thing, logging the error and sending it back to the top level.

    So you end up with a bunch of duplicates in the log file

unable to write: io.EOF
could not write config: io.EOF
...
Copy the code

\2. At the top of the program, although the original error is received, there is no relevant content, in other words, the WriteAll and WriteConfig log information is not wrapped in the error, and returned to the upper layer.

The solution to these two problems can be to add context information for errors in the low-level functions WriteAll and WriteConfig, and then send the errors back to the upper layer, where the upper layer program finally handles the errors.

An easy way to wrap an error is to use the FMT.Errorf function to add information to the error.

func WriteConfig(w io.Writer, conf *Config) error { buf, err := json.Marshal(conf) if err ! = nil { return fmt.Errorf("could not marshal config: %v", err) } if err := WriteAll(w, buf); err ! = nil { return fmt.Errorf("could not write config: %v", err) } return nil } func WriteAll(w io.Writer, buf []byte) error { _, err := w.Write(buf) if err ! = nil { return fmt.Errorf("write failed: %v", err) } return nil }Copy the code

Append context information to errors

FMT.Errorf just adds simple annotation information to the error. If you want to add information along with the error call stack, you can take advantage of the error wrapping capability provided by github.com/pkg/errors.

Func WithMessage(err error, Func WithStack(err Error) error func Wrap(err Error, message String) errorCopy the code

The Cause method will return the original error corresponding to the packing error — that is, it will recursively unpack.

func Cause(err error) error
Copy the code

Here is the error handler rewritten using github.com/pkg/errors

func ReadFile(path string) ([]byte, error) { f, err := os.Open(path) if err ! = nil { return nil, errors.Wrap(err, "open failed") } defer f.Close() buf, err := ioutil.ReadAll(f) if err ! = nil { return nil, errors.Wrap(err, "read failed") } return buf, nil } func ReadConfig() ([]byte, error) { home := os.Getenv("HOME") config, err := ReadFile(filepath.Join(home, ".settings.xml")) return config, errors.WithMessage(err, "could not read config") } func main() { _, err := ReadConfig() if err ! = nil { fmt.Printf("original error: %T %v\n", errors.Cause(err), errors.Cause(err)) fmt.Printf("stack trace:\n%+v\n", err) os.Exit(1) } }Copy the code

The above %+v format string is based on % v, expand the value, that is, expand the complex type value, such as the structure of the field value details.

In this way, the call stack information can be added to the error, and the reference to the original error can be preserved.

conclusion

To summarize, the principles of error handling are:

  1. Errors are processed only once in the outermost layer of logic, and only errors are returned at the bottom layer.
  2. In addition to returning errors, the bottom layer should wrap the original errors and add context information such as error information and call stack to facilitate troubleshooting.

I like the content and writing style of webmaster’s articles. Remember to give me amway to more people.