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 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.Print
ing 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.
Please watch https://youtu.be/lsBF58Q-DnY if you have not already.