I have recently started learning Rust and I really like the utility and brevity of the Result
enum and the ?
operator. So I tried to see how close I can get to implementing something like that in Go. In addition, I can point to this when someone says something along the lines of "Oh Go error handling is so verbose".
If you are unfamiliar with Rust, the Result
type is an enum that can hold either an Ok
valid value or an error. This is pretty neat but the real utility comes from the ?
operator. When the ?
operator is called on a Result
it will stop the execution of the code if there is an error in the Result
and simply return the Result
from the parent function. Basically Rust replaces the well known Go error handling boilerplate like below with only a single character!
if err != nil {
return nil, err
}
If there is no error in the Result
, then the operator unwraps the Ok
value and returns it.
Rust and Go are quite different, so implementing the same Result
enum and ?
operator from Rust in Go is not as simple as porting or translating the code. Here are some of the challenges I came across and how I addressed them in Go.
Enums
I don't think it is a hot take to say that Go's enums can be limiting and are not nearly as expressive as Rust's. Therefore instead of using an enum in Go for the Result
type I used generics and a struct.
type Result[T any] struct {
Ok T
Err error
}
Question mark operator
This is a bigger problem. As far as I know Go does not have any functionality similar to this. The only way to break the flow of a function "at will" is to panic. So I decided that I can add a method on the Result
struct that will panic if an error is present. But that is not all, I still need some way to recover this panic and let the error "bubble up" the call stack. The way to achieve this is to return a named Result
from the parent function, pass a pointer to this named Result
to a deferred function that will recover from the panic and place the error in this pointer to the output. Let's call this deferred function EscapeHatch
. This is how it would look like:
func EscapeHatch[T any](res *Result[T]) {
if r := recover(); r != nil {
err, ok := r.(ehError)
if !ok {
// Panicking again because the recovered panic is not an ehError
panic(r)
}
*res = Result[T]{Err: err.error}
}
}
One more thing comes up here. Namely, any panics that we raise through the ?
operator are wrapped in a simple struct so that we recover only those and for all others we panic again. Lastly, because ?
is not a valid name for a method in Go I instead used Eh
. This is short for EscapeHatch
and also something that Canadians will add at the end of a sentence to make it into a question. For example "We have been having some lovely weather, eh?". So I think it is an appropriate substitution to the ?
operator.
Demo time!
Putting everything together I can convert code like this:
func example(aFile string) ([]byte, error) {
f, err := os.Open(aFile)
if err != nil {
return nil, err
}
buff := make([]byte, 5)
_, err := f.Read(b1)
if err != nil {
return nil, err
}
return buff, nil
Into this:
func example(aFile string) (res eh.Result[[]byte]) {
defer eh.EscapeHatch(&res) // must have pointer to the named output Result
buff := make([]byte, 5)
file := eh.NewResult(os.Open(aFile)).Eh()
_ = eh.NewResult(file.Read(buff)).Eh()
return eh.Result[[]byte]{Ok: buff}
Isn't this really hacky?
I felt a lot worse about this idea initially, especially about potentially "abusing" panic in this way. But then I found out that even the json package in the standard Go library will use panic, wrap the error and recover it to stop executing when recursively encoding an interface. So if this is an anti-pattern then at least I can say the standard library is doing it too.
Shameless library plug
If you are curious I combined all the code and some utility functions in a small package here. Give it a try and let me know what you think.
Top comments (2)
Found your article while reading some Rust material and wondered if anyone had attempted this in Go.
I think it's a bit awkward using it with the standard library calls, but in theory if this
Result[T]
pattern was more popular there would probably be someone who wrapped the standard libraries to return aResult[T]
instead.I came up with something like looked like this
Here is the rest of the source for reference: github.com/zrbecker/wrap/
Very neat pattern for sure. I also found handling the panic to not feel as dirty as I thought it would.
Hi Zachary, I thought I would get notifications if someone comments on my article but I guess I did not or it just got lost in my inbox among the mostly unimportant stuff I get on a daily basis.
And yeah I agree since no other package or library is using this it gets awkward because you have to wrap every output from a function that comes from another library.
Maybe a future version of Go will support fancier enums like this and the standard Go library will have Result/Option types just like Rust 🤞.