One Way To Do It — Six Variations

Patrick Kelly
6 min readApr 2, 2018

One common saying of the Go programming language is that there’s one way to do it. Of course that’s not strictly true, but generally, the language does not support a multiple ways to do a single thing. For instance, if you want to iterate a sequence, you’ll use a for loop.

An area of shock for most go newcomers is error handling. The idea of being forced to check errors and react after every single line of doing something is nearly repulsive when coming from languages that support try ... catch style exception handling.

For this article, I’ll show “the standard way” and then five other options for handling errors in a very simple go program.

The example code is all available at github.com/pdk/oneway, and each example is runnable in the golang playground.

1. Standard Idiom

The standard go idiom is to check the error and handle it after every call that may produce an error:

// Drive to the store
shopper, err := shopper.Drive(FuelNeededToGetToStore)
if err != nil {
log.Fatalf("could not complete shopping: %s", err)
}
// Buy the eggs
shopper, err = shopper.BuyEggs(EggsRequired)
if err != nil {
log.Fatalf("could not complete shopping: %s", err)
}
// Drive home
shopper, err = shopper.Drive(FuelNeededToGetHome)
if err != nil {
log.Fatalf("could not complete shopping: %s", err)
}

Please examine the full program and try it out. You can adjust the constants at the top to see behavior with various error conditions.

2. Common Error Handler Function

Where we’re doing the same thing for every error condition, we can factor it out into a function, to reduce the repeated 3 lines of code to 1. Of course this is only an option when your error handling for every error is the same. In this example we’re aborting the program.

func FatalIfErrNotNil(err error) {
if err != nil {
log.Fatalf("could not complete shopping: %s", err)
}
}
...
shopper, err := shopper.Drive(FuelNeededToGetToStore)
FatalIfErrNotNil(err)
shopper, err = shopper.BuyEggs(EggsRequired)
FatalIfErrNotNil(err)
shopper, err = shopper.Drive(FuelNeededToGetHome)
FatalIfErrNotNil(err)

Full program runnable here.

This approach is only usable when you don’t need to actually return. If the code needs to return (leave the current function) when there’s an error, you’ll still need an if block. In that case you can factor out all the handling to a function which returns true/false so you get something like:

    shopper, err := shopper.Drive(FuelNeededToGetToStore)
if ErrorHandled(err) {
return ...
}

and other variations.

3. Include Error Check In Methods

This “solution” isn’t all that great, but is more of a step toward our later solutions. Theoretically you might do this if you really, really want to move the error handling out of your mainline logic.

By accepting the latest error into each of our methods, and having each of them do a check before doing their logic, we can have a cleaner code line, higher up in the call stack.

func (s Shopper) Drive(fuelRequired int, err error) (Shopper, error) {
if err != nil {
return s, err
}
... actual logic of this method ...
}
shopper, err := shopper.Drive(FuelNeededToGetToStore, nil)
shopper, err = shopper.BuyEggs(EggsRequired, err)
shopper, err = shopper.Drive(FuelNeededToGetHome, err)
if err != nil {
log.Fatalf("could not complete shopping: %s", err)
}

Each method only does stuff if there has not been an error yet, and we only do the real error check after attempting to do all our mainline steps.

Here it is in the playground.

4. Use Functions Instead Of Methods

This doesn’t add/change any real functionality, but is a step on our journey. :)

We can rewrite our methods as functions, just passing in the shopper as an argument, rather than using it as a method receiver.

func Drive(s Shopper, fuelRequired int, err error) (Shopper, error) {
if err != nil {
return s, err
}

That version is runnable here.

Totally pointless, really, except as a step towards

5. Use A Function Generator To Factor Out Error Handling

Since we’re now talking about function, and functions are first class citizens in goland, we have the option to refactor this into functions that do the work, and a function generator that adds in, or decorates, those functions with error handling.

Here’s our generator:

func ErrCheckFunc(f func(Shopper, int) (Shopper, error)) func(error, Shopper, int) (error, Shopper) {    return func(err error, s Shopper, arg int) (error, Shopper) {
if err != nil {
return err, s
}
s, err = f(s, arg)
return err, s
}
}

The first line of that is the worst, just because a lot of type declarations are required. ErrCheckFunc is a function that takes a single argument, f. f is a function which takes two arguments, a shopper and an integer, and returns two results, a shopper and an error. ErrCheckFunc takes that single argument and returns a single result, a new function (unnamed as yet). The function returned is going to take three arguments, an error, a shopper, and an integer. This new, generated, function will return two results, an error and a shopper.

It’s possible to make this declaration somewhat prettier with separate type definitions for the types of the functions, but it’s hard to say if that makes things easier or harder to read/understand. Anyway, that’s left as an exercise for the reader. :)

The point here is that our operational functions can focus only on what they need to do, and our function generator can be applied to them to produce new functions which have our common error handling.

Now we can say:

drive := ErrCheckFunc(Drive)
buy := ErrCheckFunc(BuyEggs)
err, shopper := drive(nil, shopper, FuelNeededToGetToStore)
err, shopper = buy(err, shopper, EggsRequired)
err, shopper = drive(err, shopper, FuelNeededToGetHome)
if err != nil {
log.Fatalf("could not complete shopping: %s", err)
}

Our new functions drive and buy are generated by wrapping our original functionality Drive and BuyEggs with a error-check short circuit.

Runnable here.

6. A Single Line Of Action

This last variation might seem a little extreme, but it demonstrates a less known feature of go that I really like.

Since we’re generating functions and using closures, how about we go ahead and capture the integer argument as well?

func ErrCheckFunc(f func(Shopper, int) (Shopper, error), arg int) func(error, Shopper) (error, Shopper) {    return func(err error, s Shopper) (error, Shopper) {
if err != nil {
return err, s
}
s, err = f(s, arg)
return err, s
}
}

The difference between this generator and the previous version is that ErrCheckFunc takes one more argument, and the generated function takes one less. I’ve moved the arg int argument to ErrCheckFunc rather than it being an input to the generated function.

Compare the previous mainline logic to this:

driveToStore := ErrCheckFunc(Drive, FuelNeededToGetToStore)
buyEggs := ErrCheckFunc(BuyEggs, EggsRequired)
driveHome := ErrCheckFunc(Drive, FuelNeededToGetHome)
err, shopper := driveHome(buyEggs(driveToStore(nil, shopper)))if err != nil {
log.Fatalf("could not complete shopping: %s", err)
}

The “cool feature” leveraged here is that when a function produces the same number and type of results as another function requires as inputs, you can directly call one on the results of the other.

Perhaps an example is in order.

func numbers() (int, int) {
return 1, 1
}
func add(a, b int) int {
return a + b
}
answer := add(numbers())

Notice how add requires two arguments, but it appears that we’re only passing it one. Actually, since numbers produces two results, they are the two inputs to add.

That’s exactly what’s going on with

err, shopper := driveHome(buyEggs(driveToStore(nil, shopper)))

Each of those three function calls take two inputs, an error and a shopper, and produce two results, an error and a shopper.

This example is somewhat trivial, but it demonstrates how it is possible to refactor things in order to make your mainline logic more concise.

Conclusion

Not much to say here. I guess the point of this exercise is to show that there are actually multiple ways to do things in go. The language is “simple” in that there is not a great number of features, but it is very powerful, allowing you to refactor things in sometimes surprising ways.

Bonus Round

After posting this, got

so I cooked up yet another variation.

func ProcessSteps(s Shopper, steps ...func(Shopper) (Shopper, error)) (Shopper, error) {
for _, oneStep := range steps {
var err error // avoid shadowing s
s, err = oneStep(s)
if err != nil {
return s, err
}
}
return s, nil
}

ProcessSteps is a function that takes a variable number of function arguments, executes them in series, checking each time for an error. This allows for this mainline logic expression:

shopper, err := ProcessSteps(shopper,
driveToStore,
buyEggs,
driveHome,
)
if err != nil {
log.Fatalf("could not complete shopping: %s", err)
}

While the suggestion was based on clojure, I think this sort of resembles a try ... catch block.

--

--

Patrick Kelly

Web/database engineer/gopher. Cycling, photos, yada, yada.