Skip to content

Instantly share code, notes, and snippets.

@fospathi
Last active May 5, 2024 16:15
Show Gist options
  • Save fospathi/1e6f5aea622abb52bddc9bcb1ffee858 to your computer and use it in GitHub Desktop.
Save fospathi/1e6f5aea622abb52bddc9bcb1ffee858 to your computer and use it in GitHub Desktop.
Why are Go's identical errors not equal?

⚠️ This discussion applies to version 1.13 (released 2019) of the Go language.

Why are Go's identical errors not equal?

Or, more precisely, why are two separate but otherwise seemingly identical errors created by Go's errors.New function not equal according to the == operator?

Consider the output of the following small program (Go Playground link) which compares Go's error values for equality:

package main

import (
	"errors"
	"fmt"
)

const message = "A custom error message"

func main() {
	var e1, e2 error

	e1 = errors.New(message)
	e2 = errors.New(message)

	fmt.Println(e1 == e2)

	// fmt.Errorf delegates its error creation to errors.New and so shares the same behaviour.
	e1 = fmt.Errorf(message)
	e2 = fmt.Errorf(message)

	fmt.Println(e1 == e2)
}

This outputs:

false
false

For a newcomer to the Go language this may come as a surprise.

But some identical errors are equal

Lets compare the surprising behaviour above to that of a simple custom error type.

To qualify as an error the type needs to implement Go's error interface:

type error interface {
    Error() string
}

So create a type with a method called Error which returns a string:

type CustomError struct {
   Message string
}

func (ce CustomError) Error() string {
   return ce.Message
}

Now consider the output of the following small program (Go Playground link) which compares our custom error values for equality:

package main

import "fmt"

type CustomError struct {
	Message string
}

func (ce CustomError) Error() string {
	return ce.Message
}

const (
	message1 = "Custom error message 1"
	message2 = "Custom error message 2"
)

func main() {
	var e1, e2 error

	e1 = CustomError{Message: message1}
	e2 = CustomError{Message: message1}

	// A type assertion shows our custom error type is a 'proper' error
	_, ok := e1.(error)

	fmt.Println(ok)
	fmt.Println(e1 == e2)

	e2 = CustomError{Message: message2}

	fmt.Println(e1 == e2)
}

This outputs:

true
true
false

Unlike Go's error type our simple struct error type can be compared using ==. Indeed, some error interface values do work with ==.

To uncover the issue with Go's own error values we will need to discover how Go implements its own error type as returned by the function errors.New.

== operator

Before delving into the internals of errors.New lets review what the Language Specification has to say about the equal comparison operator == as it applies to interfaces, structs, and pointers:

  • Interface values are comparable. Two interface values are equal if they have identical dynamic types and equal dynamic values or if both have value nil.

  • Struct values are comparable if all their fields are comparable. Two struct values are equal if their corresponding non-blank fields are equal.

  • Pointer values are comparable. Two pointer values are equal if they point to the same variable or if both have value nil. Pointers to distinct zero-size variables may or may not be equal.

Of the two component values which comprise an error interface value, the dynamic type and the dynamic value, it will be something about the dynamic value of Go's errors which prevents successful comparisons.

If the dynamic value is just a simple struct type, as is our custom error type, then == works fine.

Pointer error types

A look at errors.go over on Go's official github repository reveals that the dynamic value of error interface values returned by errors.New is a pointer to a struct:

// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
	return &errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
	s string
}

func (e *errorString) Error() string {
	return e.s
}

New returns a pointer to a new variable, irrespective of whether the string argument is the same as a previously supplied argument, and pointers are not equal unless they point to the same variable.

Go's identical errors are not identical after all.

But why?

As to why someone chose to return a pointer, who knows. Maybe to prevent this method of checking errors in favour of more idiomatic methods.

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