DEV Community

Elijah Wilson
Elijah Wilson

Posted on • Edited on

Results in Go - an experimental way of handling errors

Error handling in Go is less than straightforward. There are many examples of how to handle errors and in Go 2 there may even be changes to how errors work.

I recently started learning Rust which has the best error handling I've worked with for a programming language. There can still be panics but most often you will use the Result<T, E> type.

Your function might look like this:

pub fn parse_str(s: &str) -> Result<i32, &str> {
  let parse_result = s.parse::<i32>();
  match parse_result {
    Ok(v) => Ok(v),
    Err(_) => Err("unable to parse string"),
  }
}
Enter fullscreen mode Exit fullscreen mode

This says, parse the string input to an int value, if successful return v which is an int, otherwise return an error. Using this code would look like this:

fn main() {
    println!("Parsed! {:?}", parse_str("123").unwrap());
}
Enter fullscreen mode Exit fullscreen mode

The call to .unwrap() will return an int and if there is an error it will panic. This is useful when showing examples of code we're writing, but we generally wouldn't want to use .unwrap() in production code, unless we know something the compiler doesn't.

Rust also benefits from the match statement, which is similar to Go's switch statement. If you're familiar with Go error handling, it can be very redundant to write:

func ParseStr(s string) (int, error) {
  v, err := strconv.Atoi(s)
  if err != nil {
    return 0, errors.Wrap(err, "unable to parse string")
  }
  return v, nil
}
Enter fullscreen mode Exit fullscreen mode

In this single function it's not too bad, but when we want to use this function to say, parse 3 strings, it gets very verbose:

func main() {
  num1, err := ParseStr("123")
  if err != nil {
    panic(err)
  }

  num2, err := ParseStr("456")
  if err != nil {
    panic(err)
  }

  num3, err := ParseStr("789c")
  if err != nil {
    panic(err)
  }

  fmt.Printf("%d, %d, %d\n", num1, num2, num3)
}
Enter fullscreen mode Exit fullscreen mode

In many code examples, error handling will be omitted by using the underscore variable:

func main() {
  num, _ := ParseStr("123")
  fmt.Printf("Parsed %d\n", num)
}
Enter fullscreen mode Exit fullscreen mode

This definitely makes code examples a little bit nicer, but what if we could use a Result type like in Rust?

The main problem is that Go does not have generics, although this is being worked on for Go 2. What could we use right now?

I built a small, experimental library called results to see what this might look like.

Let's first work with some primitive scalar values like an int:

package main

import (
    "fmt"
    "strconv"

    "github.com/tizz98/results"
)

func betterParser(v string) (result results.IntResult) {
    val, err := strconv.Atoi(v)
    if err != nil {
        result.Err(err)
        return
    }

    result.Ok(val)
    return
}

func betterParser2(v string) (result results.IntResult) {
    result.Set(strconv.Atoi(v))
    return
}


func main() {
    result := betterParser("123").Unwrap()
    fmt.Printf("Got: %d\n", result)

    result2 := betterParser2("456").Unwrap()
    fmt.Printf("Got: %d\n", result2)

    // This will panic if you uncomment
    // _ = betterParser2("foo").Unwrap()
}
Enter fullscreen mode Exit fullscreen mode

Personally, I think the calls to .Unwrap() make the examples easier to read and less underscores to ignore errors. betterParser2 also shows an interesting feature of my result types: the .Set(...) method. If Go had generics, it would be defined like:

func (r *Result<T>) Set(v T, err error) {
  if err != nil {
    r.Err(err)
    return
  }
  r.Ok(v)
}
Enter fullscreen mode Exit fullscreen mode

In Go, it's very common to return a tuple of two values, the first being an arbitrary type T and the second being an error. I hope this will make it easier to use this library with other libraries. The second unique method is called .Tup() which will return a tuple of type T and an error. To allow you to call result.Tup() when working with existing code that doesn't use the result types.

How does this code work without generics? Code generation. You can create these result types by adding a line like so to your files:

//go:generate go run github.com/tizz98/results/cmd -pkg foo -t *Bar -tup-default nil -result-name BarPtrResult
//go:generate go run github.com/tizz98/results/cmd -pkg foo -t Bar -tup-default Bar{} -result-name BarResult
package foo

type Bar struct {
    Baz int
    Field string
}
Enter fullscreen mode Exit fullscreen mode

This will generate two new result types called BarPtrResult and BarResult having an internal "value" of *Bar and Bar, respectively.

My results library has generated result types for most of Go's scalar types which you could start using today. Again, this library is experimental and I'm really just hoping to get some feedback around it!

Top comments (0)