DEV Community

Cover image for Introducing: A Go package to reduce err boilerplate
Winston Puckett
Winston Puckett Subscriber

Posted on

Introducing: A Go package to reduce err boilerplate

TL;DR look at it here: https://pkg.go.dev/github.com/ivypuckett/to@v1.0.0

Go is a wonderful language, but I keep writing the same three lines of code over and over again:

if err != nil {
    return nil, err
}
Enter fullscreen mode Exit fullscreen mode

Once is fine but if I get unlucky, I may add 15 extra lines of code over the scope of a single method... Just to return errors. Having already implemented this pattern in C#, I knew that it should be possible to eliminate this boilerplate in Go.

The goal

Implement a forward pipe operator / bind monad which enables chaining multiple methods where the input of the next is the output of the last. Upon the first error returned, return that error and the zero value of the expected result. If no errors are returned, return the result of the last operation.

The pain

In C#, the forward piping is fairly easy. I created an extension method on the base object which allows any object to call .Pipe(methodName). That means someone can call 1.Pipe(Increment).Pipe(Increment) and receive 3.

Go is not an object oriented language (thank goodness), but it still has methods. My first thought was to create a struct called Chain with a method .Bind which takes in the next method and returns a new Chain object. It'd be a lot like a linked list. I found out that Go does not support generics on methods. There's no way (I've found) to create a struct with an unbound type.

The next thought was to create a Compose[T, U, V any](func(T)(U, error), fn2(U) (V, error)) func(T) (V, error) function. This kind of function would allow someone to recursively bind functions by calling Bind(fn1, Bind(fn2, fn3)). The syntax was too cumbersome though and there's a small bit of overhead with the number of function calls.

Using variable arguments and type assertions is also promising, but I didn't want to utilize reflection as there's overhead in that as well.

The semi-elegant solution

Ultimately, the interface for this is to.Bind3(input, fn1, fn2, fn3). There is only one extra character and the only overhead is the function call itself. The package provides 1-32 parameter variants.

I considered using bind.Three as it fills a sentence better, but I thought better of it as it may break a programmer's flow easier if we have to think of how to spell a word instead of just typing the new number on a keyboard.

Example

// Before
func SaveWithoutBind(input string) (path, error) {
  sanitized, err := sanitize(input)
  if err != nil {
    return "", err
  }

  cased, err := correctCasing(sanitized)
  if err != nil {
    return "", err
  }

  queried, err := getExtraInfo(cased)
  if err != nil {
    return "", err
  }

  path, err := submit(queried)
  if err != nil {
    return "", err
  }

  return path, nil
}

// After
func SaveWithBind(input string) (path, error) {
  return to.Bind4(input, 
    sanitize, 
    correctCasing, 
    getExtraInfo, 
    submit)
}
Enter fullscreen mode Exit fullscreen mode

Install

go get github.com/ivypuckett/to@v1
Enter fullscreen mode Exit fullscreen mode

Links


Image credit: https://unsplash.com/photos/people-partying-with-confetti-ZODcBkEohk8

Top comments (0)