I’ve been writing a fairly significant amount of Go for work recently, and found that it’s basically workable. But the limited expressiveness of the language, particularly around error handling, does throw a few roadblocks in the way.
Go’s approach to simplicity (in the sense of there’s one way to do something) is clearly a big driver for the language’s design. The most obvious approach of this how idiomatic Go handles errors. Here’s one example from golang.org:
The claimed value here, in contrast to languages with exceptions, is that because errors are just first class values using structured control flow operators, they are much easier to reason about. And this much is very true. However, with a function that does a significant amount of work, (as in
cmd/build.doBuild from dive), between about a third to a half of it is made up with boilerplate error handling.
Unfortunately, because the Go compiler lacks a warning for un-used
error values, unless you know the types for everything used in a function, it can be easy to confuse a function that just has some side effect, with an un-checked error. There are external tools to fill this need thankfully, such as
It’s completely reasonable to expect people to test for certain (predictable) error conditions and either compensate or fail (eg: a missing configuration file might be entirely optional), but more often than not we want to treat error conditions as fatal, at least within a certain scope (eg: per http request).
I’ve generally found it best to minimise how many places that you have to deal with error conditions, and try to have those specialise in handling and maybe recovering from those errors.
For example, functions that only talk about a single level of abstraction, tend to make code easier to read and to understand, as they tend to separate the outcome they support from how that happens. But because these are generally implemented in terms of other functions that may fail, we have to allow for that, and handle each one explicitly. To me, this forces you to write boilerplate that repetitious, and impacts clarity.
Rust’s error handling is visually a lot more compact1. So, the above example would just be:
Personally, I think that the use of a
? operator2 offers a good compromise. It’s effectively syntactic sugar for something like:
Ie: check the result, if it’s an error return it, or else just carry on with the function.
The rust compiler also has a mechanism to warn when certain types go-unused, as is the case for
Result<T, E> values that can carry either a result value or an error.
Given what we’ve said above about there only being a few places where we want to really care about errors, this makes a reasonable trade-off between being explicit (as in Go) and the implicit approach taken with exceptions. It’s possible to see that the operation can fail, but it doesn’t interrupt the visual flow of the code.
This is a good example of the kind of simplicity that rust emphasises, the sense of embodying a single concept well where you need it, and having it be unobtrusive otherwise.
Given that Go takes an awful lot from C, I can understand why it’s chosen to throw error handling right in your face, but the absence of a built-in compiler warning for un-used error results does seem like an odd choice. However, the proposed error handling improvements for Go 2 do look promising.
although as you might guess, I have an obvious bias↩
Older versions used a
try!()macro, rather than the