Skip to content

Instantly share code, notes, and snippets.

@andrewstuart
Last active May 3, 2019 10:23
Show Gist options
  • Save andrewstuart/8d60b3b830f1acd0a87abe6b2c3932d5 to your computer and use it in GitHub Desktop.
Save andrewstuart/8d60b3b830f1acd0a87abe6b2c3932d5 to your computer and use it in GitHub Desktop.
Golang: Err on the Side of Structured

Golang: Err on the Side of Structured.

TL;DR: Implement the error interface with as much contextual and relevant information as you can in a struct. Then use type switching, not equality or string parsing, to inspect the error.

if err != nil {
  if !strings.Contains(err.Error(), "reader returned negative count from Read") {
    //Do something
  }
}

var ErrShortWrite = errors.New("short write")

I can almost guarantee you've written code exactly like the code I've pasted above. After all, why not? It's practically idiomatic at this point to use errors.New() or fmt.Errorf(), or to create a few well-know error variables that make error inspection as simple as an equality test. But what if there was a better way? What if you didn't have to throw contextually helpful information on the floor just to allow your users to check a value?

The error Interface

The key implementation detail that Go, in my opinion, got 100% right is that error is just another interface. Unfortunately, this interface only allows you to expose information in the form of a string. But does that mean that the best we can do is string inspection or equality checks? Definitely not! The interface simply guarantees that at very least, you must be able to expose a string. The truth is, as usual in Go, this tiny single-method interface is vastly more powerful than probably 99% of the uses I've seen of it.

Thanks to the wonderful design of the language, we have this powerful construct:

switch err := err.(type) {
  case ErrConnectionFailed:
    if err.AttemptedConnections > MaxAttemptedConnections {
      tryAgain()
    }
  case ErrShortWrite:
    if err.BytesRemaining == err.BytesWritten {
      log.Println("We got exactly halfway!")
    }
    log.Printf("Short write: %d bytes written, %d remain\n", err.BytesWritten, err.BytesRemaining)
  default:
    log.Println(err.Error())
}

What I would love to propose (or recommend) as the best-practice, idiomatic way of writing Go errors is that we completely forget about errors.New and fmt.Errorf and move on to structured error types. After all, it's only a few more lines of code if you want to duplicate errors.New, but what you gain is the complete flexibility and communicative potential of a struct.

Interfaces are only a miminum contract. Used properly, with the reflective power of type switches, interfaces should provide you with a workable fallback, but allow you to use polymorphism to its maximum potential and throw the consumers of your errors a lot more than just some text that they can either equality test or perform slow string comparisons (after log.Printing in development just to see what they got). Structured errors are easy to implement (only one method; just replace fmt.Errorf with return fmt.Sprintf), and they'll give you so much more power over how you can react when they occur, without having to resort to string parsing.

Hopefully this strategy can be of use to you, and I'd be surprised if I'm the first to recommend it, though I have not heard someone do so myself.

@groob
Copy link

groob commented Sep 22, 2016

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment