What is the io.Reader error contract, and why is if err != nil { return } wrong there?
Read(p) can return n > 0 and a non-nil error in the same call, including io.EOF. The contract says callers should process the n bytes before looking at the error. Checking err first loses the final chunk of data. io.EOF is also a plain sentinel that readers return unwrapped, and it means clean end of input, not failure. io.ErrUnexpectedEOF signals truncated input.
func count(r io.Reader) (int, error) {
buf := make([]byte, 4096)
total := 0
for {
n, err := r.Read(buf)
total += n // consume data FIRST
if err == io.EOF {
return total, nil // normal termination
}
if err != nil {
return total, err
}
}
}
In the general case, a function returning (T, error) with a non-nil error makes T meaningless unless the documentation says otherwise. io.Reader and io.Writer (which must return a non-nil error if n < len(p)) are the notable exceptions. Higher-level helpers such as io.ReadAll, io.ReadFull and bufio.Scanner handle the contract for you. ReadAll treats EOF as success, and Scanner.Err() returns nil at EOF. Implementations should never return (0, nil) repeatedly.
More on Error Handling & panics
- Q323How does
net/httphandle a panic in a handler? What are the pitfalls? - Q324How would you design an error model for a large service, for example with Op, Kind and a wrapped cause?
- Q326Implement a wrapper error type that adds context and works with
errors.Is/As. What happens if you forgetUnwrap? - Q327What does this print? (Which side's
Ismethod gets called) - Q328What happens when a deferred function panics while the goroutine is already panicking?
- Q329What happens if the function passed to
sync.Once.Dopanics? How dosync.OnceFuncandsync.OnceValuediffer?