go / errwriter
I use a sticky error writer to simplify code that makes many sequential writes, checking for errors only at the end.
The problem
Writing to an io.Writer requires checking errors after each call:
func writeRecord(w io.Writer, r Record) error {
_, err := w.Write(r.Header)
if err != nil {
return err
}
_, err = w.Write(r.Body)
if err != nil {
return err
}
_, err = w.Write(r.Footer)
if err != nil {
return err
}
return nil
}
This is tedious and obscures the actual logic.
The pattern
Wrap the writer to remember the first error and skip subsequent writes:
package errwriter
import "io"
// Writer wraps an io.Writer, recording the first error.
// After an error, all writes become no-ops.
type Writer struct {
w io.Writer
n int64
err error
}
func New(w io.Writer) *Writer {
return &Writer{w: w}
}
func (w *Writer) Write(p []byte) (int, error) {
if w.err != nil {
return 0, w.err
}
n, err := w.w.Write(p)
w.n += int64(n)
w.err = err
return n, err
}
// Err returns the first error encountered, or nil.
func (w *Writer) Err() error {
return w.err
}
// Written returns the total bytes successfully written.
func (w *Writer) Written() int64 {
return w.n
}
Usage
Now the writing code is clean:
func writeRecord(w io.Writer, r Record) error {
ew := errwriter.New(w)
ew.Write(r.Header)
ew.Write(r.Body)
ew.Write(r.Footer)
return ew.Err()
}
Works well with fmt.Fprintf:
func writeReport(w io.Writer, data []Row) error {
ew := errwriter.New(w)
fmt.Fprintf(ew, "Report: %s\n", time.Now().Format(time.RFC3339))
fmt.Fprintf(ew, "Rows: %d\n\n", len(data))
for _, row := range data {
fmt.Fprintf(ew, "%s\t%d\n", row.Name, row.Value)
}
return ew.Err()
}
When to use
- Generating files or reports with many writes
- Binary encoding with sequential fields
- Template rendering to a writer
- Any code with 3+ sequential writes to the same destination
Attribution
This is Rob Pike's "errors are values" pattern from the Go Blog.