Description
This is a proposal to add a Result Type to Go. Result types typically contain either a returned value or an error, and could provide first-class encapsulation of the common (value, err)
pattern ubiquitous throughout Go programs.
My apologies if something like this has been submitted before, but hopefully this is a fairly comprehensive writeup of the idea.
Background
Some background on this idea can be found in the post Error Handling in Go, although where that post suggests the implementation leverage generics, I will propose that it doesn't have to, and that in fact result types could (with some care) be retrofitted to Go both without adding generics and without making any breaking changes to the language itself.
That said, I am self-applying the "Go 2" label not because this is a breaking change, but because I expect it will be controversial and, to some degree, going against the grain of the language.
The Rust Result type provides some precedent. A similar idea can be found in many functional languages, including Haskell's Either, OCaml's result, and Scala's Either. Rust manages errors quite similarly to Go: errors are just values, bubbling them up is handled at each call site as opposed to the spooky-action-at-a-distance of exceptions using non-local jumps, and some work may be needed to convert error types or wrap errors into error-chains.
Where Rust uses sum types (see Go 2 sum types proposal) and generics to implement result types, as a special case core language feature I think a Go result type doesn't need either, and can simply leverage special case compiler magic. This would involve special syntax and special AST nodes much like Go's collection types presently use.
Goals
I believe the addition of a Result Type to Go could have the following positive outcomes:
- Reduce error handling boilerplate: this is an extremely common complaint about Go. The
if err != nil { return nil, err }
"pattern" (or minor variations thereof) can be seen everywhere in Go programs. This boilerplate adds no value and only serves to make programs much longer. - Allows the compiler to reason about results: in Rust, unconsumed results issue a warning. Though there are linting tools for Go to accomplish the same thing, I think it'd be much more valuable for this to be a first-class feature of the compiler. It's also a reasonably simple one to implement and shouldn't adversely affect compiler performance.
- Error handling combinators (this is the part I feel goes against the grain of the language): If there were a type for results, it could support a number of methods for handling, transforming, and consuming results. I'll admit this approach comes with a bit of a learning curve, and as such can negatively impact the clarity of programs for people who are unfamiliar with combinator idioms. Though personally I love combinators for error handling, I can definitely see how culturally they may be a bad fit for Go.
Syntax Examples
First a quick note: please don't let the idea get too mired in syntax. Syntax is a very easy thing to bikeshed, and I don't think any of these examples serve as the One True Syntax, which is why I'm giving several alternatives.
Instead I'd prefer people pay attention to the general "shape" of the problem, and only look at these examples to better understand the idea.
Result type signature
Simplest thing that works: just add "result" in front of the return value tuple:
func f1(arg int) result(int, error) {
More typical is a "generic" syntax, but this should probably be reserved for if/when Go actually adds generics (a result type feature could be adapted to leverage them if that ever happened):
func f1(arg int) result<int, error> {
When returning results, we'll need a syntax to wrap values or errors in a result type. This could just be a method invocation:
return result.Ok(value)
return result.Err(error)
If we allow "result" to be shadowed here, it should avoid breaking any code that already uses "result".
Perhaps "Go 2" could add syntax sugar similar to Rust (although it would be a breaking change, I think?):
return Ok(value)
return Err(value)
Propagating errors
Rust recently added a ?
operator for propagating errors (see Rust RFC 243). A similar syntax could enable replacing if err != nil { return _, err }
boilerplate with a shorthand syntax that bubbles the error up the stack.
Here are some prospective examples. I have only done some cursory checking for syntactic ambiguity. Apologies if these are either ambiguous or breaking changes: I assume with a little work you can find a syntax for this which isn't at breaking change.
First, an example with present-day Go syntax:
count, err = fd.Write(bytes)
if err != nil {
return nil, err
}
Now with a new syntax that consumes a result and bubbles the error up the stack for you. Please keep in mind these examples are only for illustrative purposes:
count := fd.Write!(bytes)
count := fd.Write(bytes)!
count := fd.Write?(bytes)
count := fd.Write(bytes)?
count := try(fd.Write(bytes))
NOTE: Rust previously supported the latter, but has generally moved away from it as it isn't chainable.
In all of my subsequent examples, I'll be using this syntax, but please note it's just an example, may be ambiguous or have other issues, and I'm certainly not married to it:
count := fd.Write(bytes)!
Backwards compatibility
The syntax proposals all use a result
keyword for identifying the type. I believe (but am certainly not certain) that shadowing rules could be developed that would allow existing code using "result" for e.g. a variable name to continue to function as-is without issue.
Ideally it should be possible to "upgrade" existing code to use result types in a completely seamless manner. To do this, we can allow results to be consumed as a 2-tuple, i.e. given:
func f1(arg int) result(int, error) {
It should be possible to consume it either as:
result := f1(42)
or:
(value, err) := f1(42)
That is to say, if the compiler sees an assignment from result(T, E)
to (T, E)
, it should automatically coerce. This should allow functions to seamlessly switch to using result types.
Combinators
Commonly error handling will be a lot more involved than if err != nil { return _, err }
. This proposal would be woefully incomplete if that were the only case it helped with.
Result types are known for being something of a swiss knife of error handling in functional languages due to the "combinators" they support. Really these combinators are just a set of methods which allow us to transform and selectively behave based on a result type, typically in "combination" with a closure.
Then()
: chain together function calls that return the same result type
Let's say we had some code that looks like this:
resp, err := doThing(a)
if err != nil {
return nil, err
}
resp, err = doAnotherThing(b, resp.foo())
if err != nil {
return nil, err
}
resp, err = FinishUp(c, resp.bar())
if err != nil {
return nil, err
}
With a result type, we can create a function that takes a closure as a parameter and only calls the closure if the result was successful, otherwise short circuiting and returning itself it it represents an error. We'll call this function Then
(it's described this way in the Error Handling in Go) blog post, and known as and_then
in Rust). With a function like this, we can rewrite the above example as something like:
result := doThing(a).
Then(func(resp) { doAnotherThing(b, resp.foo()) }).
Then(func(resp) { FinishUp(c, resp.bar()) })
if result.isError() {
return result.Error()
}
or using one of the proposed syntaxes from above (I'll pick !
as the magic operator):
final_value := doThing(a).
Then(func(resp) { doAnotherThing(b, resp.foo()) }).
Then(func(resp) { FinishUp(c, resp.bar()) })!
This reduces the 12 lines of code in our original example down to three, and leaves us with the final value we're actually after and the result type itself gone from the picture. We never even had to give the result type a name in this case.
Now granted, the closure syntax in that case feels a little unwieldy/JavaScript-ish. It could probably benefit from a more lightweight closure syntax. I'd personally love something like this:
final_value := doThing(a).
Then(|resp| doAnotherThing(b, resp.foo())).
Then(|resp| FinishUp(c, resp.bar()))!
...but something like that probably deserves a separate proposal.
Map()
and MapErr()
: convert between success and error values
Often times when doing the if err != nil { return nil, err }
dance you'll want to actually do some handling of the error or transform it to a different type. Something like this:
resp, err := doThing(a)
if err != nil {
return nil, myerror.Wrap(err)
}
In this case, we can accomplish the same thing using MapErr()
(I'll again use !
syntax to return the error):
resp := doThing(a).
MapErr(func(err) { myerror.Wrap(err) })!
Map
does the same thing, just transforming the success value instead of the error.
And more!
There are many more combinators than the ones I have shown here, but I believe these are the most interesting. For a better idea of what a fully-featured result type looks like, I'd suggest checking out Rust's: