Hello everyone, I am fried fish.

Some time ago I shared an article " 10+ Official Go Proverbs, How Many Do You Know? , which sparked discussions among many friends. One of them is "Errors are values". Everyone jumps around repeatedly between "Errors are values" or "Errors are values", which is not easy to tangle.

In fact, Rob Pike, who said this sentence, used an article " Errors are values ​​[1] " to interpret the meaning of this proverb. What is it? What else can I do?

Today, I will learn about fried fish with everyone. The following "I" all represent Rob Pike, and some views are supplemented by fried fish.

It is highly recommended that you read it carefully.

background

One of the things that Go programmers, especially those new to the language, talk about a lot is how to handle errors. For the number of times the following code snippet appears, the conversation often turns into a lament.

(The major platforms complained and criticized a lot, thinking that the design is not good, etc.)

The following code:

if err != nil {
    return err
}

Scan code snippets

We recently scanned every Go open source project we could find and found that this snippet only appears once every page or two, which is less than some people think.

Nonetheless, if people still feel that they have to enter code like this often:

if err != nil

Then there must be something wrong, and the obvious target is the Go language itself (poorly designed?).

wrong understanding

Obviously this is unfortunate, misleading, and easy to correct. Maybe it's the case now that programmers new to Go will ask, "How do you handle errors?", learn this if err !=nil pattern, and stop there.

In other languages, one might use try-catch blocks or other similar mechanisms to handle errors. So many programmers think that when I would use try-catch in previous languages, I just type if err != nil in Go.

Over time, Go code collects many such if err != nil fragments, and the result feels unwieldy.

error is value

Whether or not this interpretation is appropriate, it is clear that these Go programmers miss a fundamental point about errors: Errors are values .

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

Of course, a common statement involving an error value is to test if it's nil, but there are countless other things you can do with an error value, and applying some of these other things can make your program better, removing a lot of the if err !=nil boilerplate .

The situation mentioned above occurs if you use a rote if statement to check for each error.

bufio example

Below is a simple example of the Scanner type from the bufio package. Its Scan method performs low-level I/O, which of course causes an error. However, the Scan method exposes no errors at all.

Instead, it returns a boolean and runs a separate method at the end of the scan reporting whether an error occurred.

The client code looks like this:

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

Of course, there is a nil check error, but it only appears and executes once. The Scan method could instead be defined as:

func (s *Scanner) Scan() (token []byte, error)

Then, an example of user code might be (depending on how the token is retrieved):

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

It's not that much different, but there is one important difference. In this code, the client has to check for errors on each iteration, but in the real Scanner API, the error handling is abstracted from the key API element, which is iterating over tokens.

With a real API, the client-side code thus feels more natural: loop until done, then worry about bugs.

Error handling does not obscure control flow.

Of course, what happens behind the scenes is that once Scan encounters an I/O error, it logs it and returns false. A separate method Err reports the error value when asked by the client.

While this is trivial, it's not the same as throwing around if err != nilafter or asking the client to check for errors. This is programming with wrong values. Simple programming, yes, but still programming.

It's worth emphasizing that, regardless of design, it's critical that programs check for errors, no matter where they are exposed. The discussion here is not about how to avoid checking errors, but about using the language to handle errors gracefully.

Practical discussion

The topic of duplicating error checking code came up when I attended the Fall 2014 GoCon in Tokyo. An enthusiastic Gopher, @jxck_ on Twitter, responded to the familiar lament about error checking.

He has some code, which is structured like this:

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

It is very repetitive. In real code, this code is longer and has more things to do, so it's not easy to just refactor this code with a helper function, but in this idealized form, a function literally closes Helpful for error variables:

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

This pattern works well, but needs to be turned off in every function that does the write; separate helper functions are awkward to use because the err variable needs to be maintained between calls (try it).

We can make it simpler, more general, and more reusable by borrowing ideas from the above scanning method. I mentioned this technique in our discussion, but @jxck_ didn't see how to apply it.

After a long time of communication, I asked if I could borrow his notebook and type some codes for him to see because of the language barrier.

I define an object called errWriter like this:

type errWriter struct {
    w   io.Writer
    err error
}

And wrote a method: Write. It does not need to have the standard Write signature, and is partially lowercase to highlight the difference.

The write method calls the underlying Writer's Write method and logs the first error for reference:

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

Once an error occurs, the Write method becomes useless, but the error value is saved.

Given the errWriter type and its Write method, the above code can be refactored into the following code:

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

This is cleaner and even makes the actual write order easier to see on the page than using a closure. No more confusion. Programming with error values ​​(and interfaces) makes the code better.

Most likely some other code in the same package could build on this idea and even use errWriter directly.

Also, once errWriter exists, it can do a lot more to help, especially in less user-friendly examples. It can accumulate bytes. It can condense writes into a buffer and transfer them atomically. there are more.

In fact, this pattern appears frequently in the standard library. It is used by the archive/zip and net/http packages. What's more prominent in this discussion is that the Writer of the bufio package is actually an implementation of the idea of ​​errWriter. Although bufio.Writer.Write returns an error, this is mainly to respect the io.Writer interface.

The Write method of bufio.Writer behaves like our errWriter.write method above, Flush will report an error, so our example can 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
if b.Flush() != nil {
    return b.Flush()
}

This approach has one obvious disadvantage, at least for some applications: there is no way to know how much processing is done before the error occurs. If that information is important, a more fine-grained approach is required. Usually, though, an all-or-nothing check at the end is sufficient.

Summarize

In this article we only looked at a technique to avoid duplicating error handling code.

Keep in mind that using errWriter or bufio.Writer is not the only way to simplify error handling, and it won't work in all situations.

However, the key lesson is that errors are values, and the full power of the Go programming language is available to deal with them .

Use this language to simplify your error handling.

But remember: whatever you do, check for your mistakes!

References

[1]

Errors are values: https://go.dev/blog/errors-are-values

Follow and add Fried Fish WeChat,

Get first-hand industry news and knowledge, and pull you into the exchange group👇

picture

picture

Hello, I am Jianyu. I have published the Go best-selling book "The Journey of Go Language Programming", and then I won the honor of GOP (the most opinionated expert in the field of Go). Click the blue word to see my book publishing road .

Share high-quality articles on a daily basis, output Go interviews, work experience, architecture design, and join WeChat to pull readers to the exchange group to communicate with everyone!