Exempt from error handling, eliminating errors

https://dave.cheney.net/2019/01/27/eliminate-error-handling-by-eliminating-errors
  • Transfer


Go2 aims to reduce error handling overhead, but do you know what is better than improved error handling syntax?

No need to handle errors at all. I do not say "delete your error handling code", instead I suggest changing your code so that you do not have many errors to handle.

This article draws inspiration from the “Define Errors Out of Existence” chapter of the book “ A Philosophy of Software Design ” by John Ousterhout. I will try to apply his advice to Go.

Example one


Here is a function to count the number of lines in a file:

funcCountLines(r io.Reader)(int, error) {
        var (
                br    = bufio.NewReader(r)
                lines int
                err   error
        )
        for {
                _, err = br.ReadString('\n')
                lines++
                if err != nil {
                        break
                }
        }
        if err != io.EOF {
                return0, err
        }
        return lines, nil
 }

We create a bufio.Reader, then we sit in a loop, calling the ReadString method, incrementing the counter until we reach the end of the file, and then return the number of lines read. This is the code we wanted to write; instead, CountLines is complicated by error handling.

For example, there is such a strange construction:

_, err = br.ReadString('\n')
lines++
if err != nil {
  break
}

We increase the number of lines before checking the error - it looks weird. The reason we have to write it this way is because ReadString will return an error if it encounters the end of the file — io.EOF — before clicking on the newline character. This can also occur if there is no newline character.

To solve this problem, we reorganize the logic to increase the number of rows, and then see if we need to exit the loop (this logic is still not correct, can you detect an error?).

But we have not finished checking errors. ReadString will return io.EOF when it reaches the end of the file. This is expected, ReadString needs some way to say stop, there’s nothing more to read. Therefore, before returning the error to the caller of the CountLine, we need to check if io.EOF was an error, and in this case return it to the caller, otherwise we will return nil when everything is fine. That's why the last line of the function is not easy

return lines, err

I think this is a good example of observing Russ Cox that error handling can make it difficult for the function to work . Let's look at the improved version.

funcCountLines(r io.Reader)(int, error) {
        sc := bufio.NewScanner(r)
        lines := 0for sc.Scan() {
                lines++
        }
        return lines, sc.Err()
}

This improved version of the transition from using bufio.Reader to bufio.Scanner. Under the hood, bufio.Scanner uses bufio.Reader, adding an abstraction layer that helps to eliminate error handling that made it difficult for our previous version of CountLines (bufio.Scanner can scan any template, by default it searches for new lines).

The sc.Scan () method returns true if the scanner found a string of text and did not detect an error. Thus, the body of our for loop will only be called when there is a line of text in the scanner's buffer. This means that our converted CountLines correctly handles the case when there is no terminating newline. Also now correctly handled the case when the file is empty.

Secondly, because sc.Scan returns false when an error occurs, our for loop ends when the end of the file is reached or an error occurs. The bufio.Scanner type remembers the first error detected by the error, and we correct this error after exiting the loop using the sc.Err () method.

Finally, buffo.Scanner takes care of handling io.EOF and converts it to nil if the end of the file is reached without an error.

Example two


My second example is inspired by the Errors are values posting on Rob Pikes blog.

When working with opening, writing and closing files, error handling is, but not very impressive, since operations can be included in helpers such as ioutil.ReadFile and ioutil.WriteFile. However, when working with low-level network protocols, it is often necessary to build a response directly using I / O primitives, so error handling can begin to repeat. Consider this fragment of the HTTP server that creates the HTTP / 1.1 response:

type Header struct {
        Key, Value string
}
type Status struct {
        Code   int
        Reason string
}
funcWriteResponse(w io.Writer, st Status, headers []Header, body io.Reader)error {
        _, err := fmt.Fprintf(w, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)
        if err != nil {
                return err
        }
        for _, h := range headers {
                _, err := fmt.Fprintf(w, "%s: %s\r\n", h.Key, h.Value)
                if err != nil {
                        return err
                }
        }
        if _, err := fmt.Fprint(w, "\r\n"); err != nil {
                return err
        } 
        _, err = io.Copy(w, body) 
        return err
}

First we create the status bar using fmt.Fprintf and check the error. Then, for each header, we write the key and value of the header, checking the error each time. Finally, we end the header section with additional \ r \ n, check the error and copy the response body to the client. Finally, although we do not need to check for an error from io.Copy, we need to convert it from a form with two return values, which io.Copy returns to one return value that WriteResponse expects.

This is not only a lot of repetitive work, every operation, which is essentially a record of bytes in io.Writer, has a different form of error handling. But we can ease our task by presenting a small type-wrapper.

type errWriter struct {
        io.Writer
        err error
}
func(e *errWriter)Write(buf []byte)(int, error) {
        if e.err != nil {
                return0, e.err
        }
        var n int
        n, e.err = e.Writer.Write(buf)
        return n, nil
}

errWriter executes the io.Writer contract, so it can be used to transfer an existing io.Writer. errWriter transmits the recordings to its underlying recorder until an error is detected. From this point on, it discards any entries and returns the previous error.

funcWriteResponse(w io.Writer, st Status, headers []Header, body io.Reader)error {
        ew := &errWriter{Writer: w} 
        fmt.Fprintf(ew, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)
        for _, h := range headers {
                fmt.Fprintf(ew, "%s: %s\r\n", h.Key, h.Value)
        }
        fmt.Fprint(ew, "\r\n")
        io.Copy(ew, body)
        return ew.err
}

Applying errWriter to WriteResponse greatly improves the clarity of the code. Each of the operations no longer needs to limit itself to error checking. The error message moves to the end of the function, checking the ew.err field and avoiding the annoying translation of the io.Copy return values

Conclusion


When you encounter excessive error handling, try extracting some operations as an auxiliary type wrapper.

about the author


The author of this article, Dave Cheney , is the author of many popular Go packages, for example github.com/pkg/errors and github.com/davecheney/httpstat .

Also popular now: