Skip to content
This repository was archived by the owner on Sep 30, 2021. It is now read-only.

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

One less if err != nil { return err }

Go is famous for its verbose error handling (only because it coerces you to handle every case) but there are some patterns to keep it under control.

This isn't meant to be a Ten Commandments of Error Handling or even a good idea; it's just a pattern than has been useful to me.

A common scenario for poor error handling in practice is when using io operations. Specifically, writes typically blow out the length of your method with repetitive error checking or they simply go without error handling at all.

We've all see this code:

package main

import "fmt"

func main() {
    fmt.Println("hello world")
}

What's the one thing missing? Error handling. Yes, I concede, it's not exactly necessary when writing to os.Stdout (this is what happens under the hood in fmt.Println). What if we're writing to a file and we do need to make sure it succeeds?

Easy. Let's check for errors when we write to the file:

package main

import "fmt"

func main() {
    // create a file and check for errors
    f, err := os.Create("./helloWorld.txt")
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close()

    // write to file and check for errors
    _, err = fmt.Fprintln("hello world")
    if err != nil {
        log.Fatal(err)
    }
}

Cool beans. What if the content we're writing is a little more sophisticated and requires multiple successive writes? Maybe we're implementing our own JSON stringifier.

Let's implement a naive stringifier that takes a list of strings and prints it to a writer as a JSON array.

func Stringify(w io.Writer, a []string) (n int, err error) {
    w.Write([]byte{'['})
    for i := 0; i < len(a); i++ {
        if i > 0 {
            fmt.Fprint(w, ", ")
        }
        fmt.Fprintf(w, `"%s"`, a[i])
    }
    w.Write([]byte{']'})
    return
}

Now, we get a lovely JSON array:

func main() {
    Stringify(os.Stdout, []string{"hello", "world"})
}
["hello", "world"]

This will work. But Stringify always returns 0, nil, no matter what happens. We need some bookkeeping around each write:

func Stringify(w io.Writer, a []string) (n int, err error) {
    var nn int

    // write opening bracket
    nn, err = w.Write([]byte{'['})
    if err != nil {
        return
    }
    n += int64(nn)

    for i := 0; i < len(a); i++ {
        if i > 0 {
            // write separator
            nn, err = fmt.Fprint(w, ", ")
            if err != nil {
                return
            }
            n += int64(nn)
        }

        // write quoted member
        nn, err = fmt.Fprintf(w, `"%s"`, a[i])
        if err != nil {
            return
        }
        n += int64(nn)
    }

    // write closing bracket
    nn, err = w.Write([]byte{']'})
    if err != nil {
        return
    }
    n += int64(nn)

    return
}

Oof, that's verbose! We're only doing four writes. You can see why so many gophers might opt for ignoring the occasional cheaky error.

There is a more elegant way to solve this that leverages the power of Go's io.Writer interface. Let's create a middleware writer that wraps our output writer, keeps track of the number of bytes written and handles any errors along the way.

Because naming is hard, we'll call it the AggregatedWriter:

type AggregatedWriter struct {
    w   io.Writer // underlying writer
    n   int64     // cumulative total of bytes written to w
    err error     // any error returned by w
}

func NewAggregatedWriter(w io.Writer) *AggregatedWriter {
    if ag, ok := w.(*AggregatedWriter); ok {
        return ag // w was already an AggregatedWriter
    }
    return &AggregatedWriter{w: w}
}

func (w *AggregatedWriter) Write(p []byte) (n int, err error) {
    if w.err != nil {
        return 0, w.err
    }
    n, err = w.w.Write(p)
    w.n += int64(n)
    w.err = err
    return
}

func (w *AggregatedWriter) N() int64                     { return w.n }
func (w *AggregatedWriter) Err() error                   { return w.err }
func (w *AggregatedWriter) Result() (n int64, err error) { return w.n, w.err }

The implementation is very simple but we hopefully get a lot of miles out of it.

The AggregatedWriter implements io.Writer so we can use it anywhere you write to a file or buffer. Let's use it in our Stringify method and then talk about how it works.

func Stringify(w io.Writer, a []string) (n int, err error) {
    w = NewAggregatedWriter(w)
    w.Write([]byte{'['})
    for i := 0; i < len(a); i++ {
        if i > 0 {
            fmt.Fprint(w, ", ")
        }
        fmt.Fprintf(w, `"%s"`, a[i])
    }
    w.Write([]byte{']'})
    return w.(*AggregatedWriter).Result()
}

It's not much longer that our first example, only this time the caller gets a valid byte count and error result!

Looking into our Stringify method, it first wraps the given writer in an AggregatedWriter, performs writes as before (with no error checking) and then returns a result from the AggregatedWriter which will be the cumulative byte count of all writes and any errors that occured writing to os.Stdout.

Let's take a closer look at the AggregatedWriter:

// Write implements the io.Writer interface.
func (w *AggregatedWriter) Write(p []byte) (n int, err error) {
    // check if an error occurred previously - if so, abort this write and
    // return the previous error
    if w.err != nil {
        return 0, w.err
    }

    // write to the underlying writer and copy the bytes written and any
    // error to our return values
    n, err = w.w.Write(p)

    // increment the total written across all calls to this method
    w.n += int64(n)

    // store any error
    w.err = err

    // return (n, err) declared in the func signature
    return
}

There is a trade-off to this approach that you may need to pay attention to: execution is not interrupted when a write fails. The right way to use this is to do all your writes, check that they worked, then do stuff that depends on it. If that's not feasible, this approach might not be the right choice.

I hope this was at least an interesting exploration into one pattern for terse handling of similar errors and for stacking io.Writer interfaces on top of each other.

Give your dog a pat for me.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages