After watching Brad Fitzpatrick present Go at a recent Airbnb meetup I was inspired to give it a try again. There's a lot to like in Go: goroutines for frustration-free concurrency, strong typing with type inference, a straightforward type system, and so forth.
However, my previous attempt at Go was frustrated by the same issue that frustrated me on this attempt: error handling. The designers of Go have a philosophical objection to using control structures to handle exceptions, and I certainly can't argue with the observation that it tends to encourage spooky-action-at-a-distance, as well as abuse of the stack-unwinding behavior in non-exceptional situations, like missing files or even the crazy StopIteration exception in Python.
The idiomatic Go approach to error handling is to use the language's multi-value return capability, so many functions return exactly two values: the return value as you'd think of it in other languages, and the optional error value. This leads to code like the following:
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
dst, err := os.Create(dstName)
if err != nil {
return
}
written, err = io.Copy(dst, src)
dst.Close()
src.Close()
}
Here we have a CopyFile function that is built opon some file-handling functions in Go's standard library. Each of these functions can possibly return an error, so for each of them we must test for the error case and propagate the error up the stack to the caller via a normal return statement.
This approach has the advantage of making the stack unwinding on error very explicit; if all of the intermediate stack frames follow the same pattern then this is functionally equivalent to the automatic stack unwinding performed by statements like throw or raise in other languages, but in Go each stack frame must be in on the joke so that you can see clearly the control flow in both normal and exceptional cases.
However, this pattern causes code to be littered with variations on the following over and over again, often distracting from the much-more-interesting code in between:
if err != nil {
return
}
Indeed, in my CopyFile example the error handling takes up over half of a function that otherwise contains only five useful statements. An alternative to this is to use Go's special two-clause if statement to combine the function call with the error handling, like this:
if src, err := os.Open(srcName); err != nil {
return
}
This saves a line, but to my eye this obscures both the function call and the if condition by blurring them into the same line. Perhaps this is just something that Go programmers grow accustomed to over time, but I'd sooner have the function call — the main interesting part of this fragment of code — occupy a line of its own so that it stands out and doesn't get lost in the error-handling noise.
I can't help but think that this idiom is so common in Go that it deserves some syntactic sugar in spite of the fact that there's no hidden stack unwinding going on. Certainly Go has plenty of clever syntax tricks I never knew I needed, allowing all manner of things to be implied rather than explicitly stated, so I'm surprised that the language offers no shorthand for this particular pattern.
I have a modest proposal for a shorthand for the above pattern that preserves the explicit control flow but improves the signal/noise ratio of code that uses it:
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
return on err
dst, err := os.Create(dstName)
return on err
written, err = io.Copy(dst, src)
dst.Close()
src.Close()
}
The return on variant of return would take a value that must be compatible with the interface error, and would return only if this value is non-nil. Constraining the value to be a kind of error ensures that it can be understood as error handling in all cases, and never as a general shorthand for a conditional return.
For cases where explicit return values are desired (the above example is exploiting the fact that return values can act like local variables in Go) this syntax can proceed in the same manner as for a normal return case: return on err 0, someOtherError.
I think a shorthand of this kind would go a long way to improving the readability of most Go code, allowing the reader to quickly recognize the error handling without it being a three-line eyesore hanging off every function call.