Go Best Practices — Error handling
This is the first article in a series of lessons I’ve learnt over the couple years I’ve worked with Go in production. We are running a good number of Go services in production at Saltside Technologies (psst, I’m hiring for multiple positions in Bangalore for Saltside) and I’m also running my own business where Go is an integral part.
We will cover a broad range of subjects, large and small.
The first subject I wanted to cover in this series is error handling. It’s often causing confusion and annoyance for new Go developers.
Some background — The error interface
Just so we’re on the same page. As you may know an error in Go is simply anything that implements the error
interface. This is what the interface definition looks like:
type error interface {
Error() string
}
So anything that implements the Error() string
method can be used as an error.
Checking for errors
Using error structs and type checking
When I started writing Go I often did string comparisons of error messages to see what the error type was (yes, embarrassing to think of but sometimes you need to look back to go forward).
A better approach is to use error types. So you can (of course) create structs that implements the error
interface and then do type comparison in a switch
statement.
Here’s an example error implementation.
type ErrZeroDivision struct {
message string
}func NewErrZeroDivision(message string) *ErrZeroDivision {
return &ErrZeroDivision{
message: message,
}
}func (e *ErrZeroDivision) Error() string {
return e.message
}
Now this error can be used like this.
func main() {
result, err := divide(1.0, 0.0)
if err != nil {
switch err.(type) {
case *ErrZeroDivision:
fmt.Println(err.Error())
default:
fmt.Println("What the h* just happened?")
}
} fmt.Println(result)
}func divide(a, b float64) (float64, error) {
if b == 0.0 {
return 0.0, NewErrZeroDivision("Cannot divide by zero")
} return a / b, nil
}
Here’s the Go Play link for the full example. Notice the switch err.(type)
pattern, which makes it possible to check for different error types rather than something else (like string comparison or something similar).
Using the errors package and direct comparison
The above approach can alternatively be handled using the errors
package. This approach is recommendable for error checks within the package where you need a quick error representation.
var errNotFound = errors.New("Item not found")func main() {
err := getItem(123) // This would throw errNotFound
if err != nil {
switch err {
case errNotFound:
log.Println("Requested item not found")
default:
log.Println("Unknown error occurred")
}
}
}
This approach is less good when you need more complex error objects with e.g. error codes etc. In that case you should create your own type that implements the error
interface.
Immediate error handling
Sometimes I come across code like the below (but usually with more fluff around..):
func example1() error {
err := call1() return err
}
The point here is that the err
is not handled immediately. This is a fragile approach since someone can insert code between err := call1()
and the return err
, which would break the intent, since that may shadow the first error. Two alternate approaches:
// Collapse the return and the error.
func example2() error {
return call1()
}// Do explicit error handling right after the call.
func example3() error {
err := call1()
if err != nil {
return err
} return nil
}
Both of the above approaches is fine with me. They achieve the same thing, which is; if someone needs to add something after call1()
they need to take care of the error handling.
That’s all for today
Stay tuned for the next article about Go Best Practices. Go strong :).
func main() {
err := readArticle("Go Best Practices - Error handling")
if err != nil {
ping("@sebdah")
}
}