Source: www.cyningsun.com/08-19-2019/…

A common discussion point among Go programmers, especially those new to the language, is how to handle errors. Conversations often turn into frustration at the number of times the following snippets of code appear

iferr ! =nil {
    return err
}
Copy the code

We recently scanned every open source project we could find and found that this snippet of code only happens once per page or two, less than you might think. Nonetheless, if you have to always write

iferr ! = nuilCopy the code

The feeling that something is wrong persists, and the obvious target is Go itself.

This is regrettable and misleading, and can easily be corrected. That’s probably what new Go programmers are asking: “How do you handle bugs?” They hit this pattern and they stop there. In other languages, try-catch blocks or other such mechanisms can be used to handle errors. So programmers think that when I use try-catch in older languages, I just type if err! In Go. = nil. Over time, the Go code assembles many of these pieces, and the result is unwieldy.

Whether or not this explanation is appropriate, it is clear that these Go programmers are missing one fundamental point about Errors: Errors are values.

Values can be programmed, and since errors are values, errors can be programmed.

Of course, the common statement involving an error value is to check if it is nil, but there are countless other things you can do with an error value, and applying some of them can make your program better, eliminating a lot of boilerplate that would occur if you mechanically used an if statement to check for every error.

Here is a simple example of the Bufio package Scanner type. Its Scan method performs low-level I/O, which of course leads to errors. However, this Scan method does not expose errors at all. Instead, it returns a Boolean value and a separate method, run at the end of the scan, to report whether an error occurred. The client code looks like this:

scanner := bufio.NewScanner(input)
for scanner.Scan() {
    token := scanner.Text()
    // process token
}
iferr := scanner.Err(); err ! =nil {
    // process the error
}
Copy the code

Of course, there is a null value check that fails, but it only occurs and is performed once. The Scan method can be defined as

func (s *Scanner) Scan(a) (token []byte, error)
Copy the code

Then the sample user code might be (depending on how the token is retrieved),

scanner := bufio.NewScanner(input)
for {
    token, err := scanner.Scan()
    iferr ! =nil {
        return err // or maybe break
    }
    // process token
}
Copy the code

There’s not much difference, but there’s an important difference. In this code, the client must check for errors on each iteration, but in the real Scanner API, error handling is abstracted from the key API element, which is iterating over tokens. With a real API, the client-side code is more natural: loop until done, then error handling at the end. Error handling does not obscure the control flow.

Behind the scenes, of course, whenever Scan encounters an I/O error, it logs it and returns false. A single Err method reports an error value when called by the client. Although very trivial, it is with knocking everywhere

iferr ! =nil
Copy the code

Or require the client to check for different errors after each token. It’s programming with an error value. Simple programming, yes, still programming.

It is important to emphasize that program error checking is critical regardless of design. The discussion here is not about avoiding errors, but about using language to gracefully handle errors.

When I attended GoCon in Tokyo in fall 2014, the theme of repetitive error checking codes came up. An enthusiastic Gopher, known on Twitter as @jxck, echoed the familiar frustration with error checking. He has some code that looks like this:

_, err = fd.Write(p0[a:b])
iferr ! =nil {
    return err
}
_, err = fd.Write(p1[c:d])
iferr ! =nil {
    return err
}
_, err = fd.Write(p2[e:f])
iferr ! =nil {
    return err
}
// and so on
Copy the code

The code is very repetitive. In real code, it would be much longer and much more, so it’s not easy to refactor it using helper functions, but in such ideal cases it’s useful to encapsulate the function literal of the error variable:

var err error
write := func(buf []byte) {
    iferr ! =nil {
        return
    }
    _, err = w.Write(buf)
}
write(p0[a:b])
write(p1[c:d])
write(p2[e:f])
// and so on
iferr ! =nil {
    return err
}
Copy the code

This pattern works, but each function that performs a write requires a closure; Separate helper functions are awkward to use because err variables need to be maintained across calls (try).

By borrowing ideas from the Scan approach above, we can make our code cleaner, more generic, and reusable. I mentioned this technique in the discussion, but @jxCK didn’t figure out how to apply it. After a long exchange, hampered by the language barrier, I asked if I could borrow his laptop and show him some code by writing it.

I define an object called errWriter, as follows:

type errWriter struct {
    w   io.Writer
    err error
}
Copy the code

And give it a method, write. The lowercase part is intended to highlight the difference and does not require a standard Write signature. The write method calls the underlying Writer’s write method and logs the first error for future reference:

func (ew *errWriter) write(buf []byte) {
    ifew.err ! =nil {
        return
    }
    _, ew.err = ew.w.Write(buf)
}
Copy the code

Once an error occurs, the write method becomes operation-free, but the error value is saved.

With the errWriter type and its write method, you can refactor the above code as follows:

ew := &errWriter{w: fd}
ew.write(p0[a:b])
ew.write(p1[c:d])
ew.write(p2[e:f])
// and so on
ifew.err ! =nil {
    return ew.err
}
Copy the code

It’s even cleaner now than before with closures, and it’s easier to see the actual write order on the paper. No more clutter. Programming with error values (and interfaces) makes your code better.

Perhaps code elsewhere in the same package can use this idea, or even use errWriter directly.

Furthermore, once errWriter exists, it can do much more, especially in more practical examples. It can accumulate bytes. It can merge writes into a buffer that can then be transferred atomically. And so on.

In fact, this pattern often appears in standard libraries. Archive /zip and NET/HTTP packages are in use. What is most striking about this discussion is that the Writer of the Bufio package is really an implementation of the idea of errWriter. Although bufio.writer. Write returns an error, it is mainly to implement the IO.Writer interface. The Write method of bufio.writer, like our errwriter.write method above, Flush reports an error, so our example could be written like this:

b := bufio.NewWriter(fd)
b.Write(p0[a:b])
b.Write(p1[c:d])
b.Write(p2[e:f])
// and so on
ifb.Flush() ! =nil {
    return b.Flush()
}
Copy the code

For at least some applications, this approach has an obvious disadvantage: there is no way to know how much processing has been done until an error occurs. If this information is important, a more fine-grained approach is required. But, in the end, all or nothing is usually enough.

We examined only one technique to avoid repeating error-handling code. Keep in mind that using errWriter or Bufio. Writer is not the only way to simplify error handling, and it is not appropriate for all cases. The key lesson, however, is that errors are values, and the full power of the Go programming language is available to handle them.

Use language to simplify error handling.

But remember: no matter what you do, always check your mistakes!

Finally, for a full story on my interaction with @Jxck, including a short video he recorded, visit his blog.