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 panic
s 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"),
}
}
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());
}
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
}
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)
}
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)
}
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()
}
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)
}
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
}
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)