One Way To Do It — Six Variations
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.
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.
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.