Elegant error handling in golang

Time:2020-5-23

The error handling of golang has been criticized by everyone. Half of the code in the project is doing error handling.

After a period of development in golang, I also feel the same way. I think it’s necessary to optimize it. On the one hand, it makes the code more elegant. On the other hand, it’s also to form the system’s error handling mode, rather than come here at will errors.new (), or always return err.

After consulting some data, I found that my understanding of golang error handling is still at a low level. I’d like to discuss with you and consolidate what I have learned

Error return processing

In multi-level function calls, my common processing methods are:

func Write(w io.Writer, buf []byte) error {
  _, err := w.Write(buf)
  if err != nil {
    // annotated error goes to log file
    log.Println("unable to write:", err)
  
    // unannotated error returned to caller
    return err
  }
  return nil
}

It’s very convenient to add logs layer by layer for fault location, but in this way, there will be many repeated error descriptions in the log file, and the error obtained by the upper calling function is still the error returned by the lower function, without context information

Optimization 1:

func Write(w io.Writer, buf []byte) error {
  _, err := w.Write(buf)
  if err != nil {
    // annotated error returned to caller
    fmt.Errorf("authenticate failed: %v", err)
  }
  return nil
}

The duplicate error log is removed here, and the context information is added to the error information returned to the upper calling function. But doing so destroys the equality detection, that is, we cannot judge whether the error is a pre-defined error.

For example:

func main() {
    err := readfile(“.bashrc”)
    if strings.Contains(error.Error(), "not found") {
        // handle error
    }
}

func readfile(path string) error {
    err := openfile(path)
    if err != nil {
        return fmt.Errorf(“cannot open file: %v", err)
    }
    // ……
}

As a result, the caller has to use string matching to determine whether the underlying function readfile has some errors.

Optimization II:

Use third-party libraries:github.com/pkg/errorsWrap can add a string to an error and wrap it into a new string. Cause do the opposite.

// Wrap annotates cause with a message.
func Wrap(cause error, message string) error
// Cause unwraps an annotated error.
func Cause(err error) error

For example:

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
}

Wrap can not only contain the context information of the underlying called function, but also restore the error through cause to judge the original error type, as follows:

func main() {
    _, err := ReadFile()
    if errors.Cause(err) != io.EOF {
        fmt.Println(err)
        os.Exit(1)
    }
}

A similar error handling function has been added to go1.13 just released this year

//Go1.13 does not provide the wrap function, but through fmt.Errof Provides similar functionality
fmt.Errorf("context info: %w",err)

//To resolve the nested error, you need to call the unwrap function multiple times to get the innermost error
func Unwrap(err error) error

abnormal

When some developers write code, they do not distinguish between exceptions and errors, and they treat them as errors. This way is not elegant. It is flexible to use the built-in functions panic and recover of golang to trigger and terminate exception handling process.

Error refers to the possible problems, such as failure to open a file, which is expected by people; Exception refers to the problems that should not occur, such as the reference to a null pointer, which is not expected by people.

Here are some scenarios where exceptions should be thrown:

  1. Null pointer reference
  2. Subscript out of range
  3. Divisor is 0
  4. Branches that should not appear, such as default
  5. Input should not cause function errors

In the process of application development, by throwing panic exception, the program exits and finds problems in time. After deployment, to ensure the continuous and stable operation of the program, it is necessary to catch exceptions through recover in time. In recover, exceptions should be handled in a reasonable way, such as:

  1. Print call information and business information of the stack for easy recording and troubleshooting
  2. Convert the exception to an error and return it to the upper caller for handling

For example:

func funcA() (err error) {
    defer func() {
        if p := recover(); p != nil {
            fmt.Println("panic recover! p:", p)
            str, ok := p.(string)
            if ok {
                err = errors.New(str)
            } else {
                err = errors.New("panic")
            }
            debug.PrintStack()
        }
    }()
    return funcB()
}

func funcB() error {
    // simulation
    panic("foo")
    return errors.New("success")
}

Recommended Today

Configure Apache to support PHP in the Apache main configuration file httpd.conf Include custom profile in

In Apache’s main configuration file / conf/ http.conf Add at the bottom Include “D:workspace_phpapache-php.conf” The file path can be any In D: workspace_ Create under PHP file apache- php.conf file Its specific content is [html] view plain copy PHP-Module setup LoadFile “D:/xampp/php/php5ts.dll” LoadModule php5_module “D:/xampp/php/php5apache2_2.dll” <FilesMatch “.php$”> SetHandler application/x-httpd-php </FilesMatch> <FilesMatch “.phps$”> SetHandler application/x-httpd-php-source </FilesMatch> […]