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 " to interpret the meaning of this proverb. What is it?
Today, I will learn to fry fish with everyone. The following "I" represent Rob Pike.
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 occurrences of the following code snippets, the conversation often turns into a lament (the major platforms complain and criticize a lot, thinking that the design is not good).
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 the pattern, and stop there.
In other languages, one might use try-catch blocks or other similar mechanisms to handle errors. So the programmer thought that I would just type if err != nil in Go when I would have used try-catch in previous languages.
Over time, Go code collects many of these snippets, and it turns out to feel 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 boilerplate.
This is what happens if you use a rote if statement to check for every error (that is, if err != nil everywhere).
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 after each if err != nil
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 gave it 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!
The article is updated continuously, you can read it by searching on WeChat [Brain fried fish]. This article has been included in GitHub github.com/eddycjy/blog . If you are learning Go language, you can see the Go learning map and route . Welcome to Star to urge you to update.
Go Book Series
- Introduction to the Go language series: a preliminary exploration of the actual combat of the Go project
- Go Programming Journey: Deep Dive into Go Projects
- Go Language Design Philosophy: Understanding Go Why and Design Thinking
- Go Language Advanced Tour: Go deeper into the Go source code
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。