Concise error handling in Go with Rust-like Result types

olevski

Tasko Olevski

Posted on June 10, 2023

Concise error handling in Go with Rust-like Result types

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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}
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
olevski
Tasko Olevski

Posted on June 10, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related