Skip to content
gohacks.dev
Basics

Comparable Types and Equality in Go Explained Properly

Which Go types support ==, why slices, maps and functions do not, how interfaces and the comparable constraint can still panic, and how ordering differs.

7 min read
Summarize in AI
Flat illustration of an editor window full of code.

Equality looks like the simplest operation in a programming language, and Go's rules for it are an annoyance that turns out to protect you from a lot. Every type either supports == or it does not, and the compiler tells you which. The cases in between, interfaces and generics, are where the rule leaks into run time. This article lays out the complete list, shows the compiler's messages for the types that fail, then goes through the two places where "comparable" does not guarantee "safe to compare". It is the background for comparing structs, slices and maps, for map keys in the maps guide and for the constraint you will meet in generics. It sits in the Go basics path.

Tested with Go 1.25.6, macOS on Apple Silicon (arm64)

What the spec says is comparable

The specification's comparison section is a short list:

TypeEqual when
Boolean, integer, float, complexThe values are equal (NaN is never equal to anything)
StringThe bytes are identical
PointerBoth point at the same variable, or both are nil
ChannelBoth were created by the same make, or both are nil
ArrayAll elements are equal, and the element type is comparable
StructAll non-blank fields are equal, and every field type is comparable
InterfaceSame dynamic type and equal dynamic values

Three types are missing, and for them the only comparison allowed is with nil:

main.goGo
package main

import "fmt"

func main() {
	a, b := []int{1}, []int{1}
	fmt.Println(a == b)

	m1, m2 := map[string]int{}, map[string]int{}
	fmt.Println(m1 == m2)

	f1, f2 := func() {}, func() {}
	fmt.Println(f1 == f2)

	fmt.Println(a == nil, m1 == nil, f1 == nil)
}
Text
# example.test/lab
./main.go:7:14: invalid operation: a == b (slice can only be compared to nil)
./main.go:10:14: invalid operation: m1 == m2 (map can only be compared to nil)
./main.go:13:14: invalid operation: f1 == f2 (func can only be compared to nil)

Each of the first three lines is rejected with X can only be compared to nil, where X is slice, map or func. The last line is the permitted form, and it compiles in a different program, since this one stops at the first three errors. The reason for the omission is that equality for these has no single good definition. Two slices could be equal if they have the same elements, or the same backing array, or the same elements and capacity. Two functions are equal if they run the same code with the same captured variables, which cannot be decided in general. The designers left the decision to you, and the standard library offers slices.Equal, maps.Equal and so on for the sensible meaning, as the comparison how-to lists.

A struct or array that contains any of the three inherits the restriction. A struct{ Name string; Tags []string } is not comparable, and cannot be a map key, and one extra field makes the whole type fall out of the comparable set, which is worth remembering when you add a field to a type that you use as a key.

Running the comparable cases

For the types that do compare, here is each one with its edge:

main.goGo
package main

import (
	"fmt"
	"math"
)

type Point struct{ X, Y int }

func main() {
	fmt.Println("abc" == "abc", "a" < "b", "Z" < "a")

	x, y := 1, 1
	px, py, pz := &x, &y, &x
	fmt.Println(px == py, px == pz, *px == *py)

	ch1, ch2 := make(chan int), make(chan int)
	fmt.Println(ch1 == ch2, ch1 == ch1)

	fmt.Println([2]int{1, 2} == [2]int{1, 2}, Point{1, 2} == Point{1, 3})

	nan := math.NaN()
	fmt.Println(nan == nan, nan != nan)

	var a, b any = 1, 1.0
	fmt.Println(a == b, a == any(1), any(int32(1)) == any(int64(1)))
}
Text
true true true
false true true
false true
true false
false true
false true false

Strings compare byte by byte, which is why "Z" < "a": uppercase ASCII sorts before lowercase. Pointers are equal only if they point at the same variable, so px == py is false even though both integers hold 1, and dereferencing first compares the values. Channels compare by identity. Arrays and structs compare element by element. NaN is the exception to "a value equals itself", which is why a map keyed by floats can leak entries. The last line shows interface equality at work: any(1) and any(1.0) are different because the dynamic types differ, an int and a float64, and the same value in a different integer type is not equal either. When you compare any values, you are comparing types as well.

Interfaces can panic

An interface type is comparable, so == compiles for any two interface values. The comparison is performed at run time, on the dynamic values, and if they are of a type that is not comparable, the program panics:

main.goGo
package main

import "fmt"

func main() {
	var a, b any = []int{1}, []int{1}
	fmt.Println(a == b)
}
Text
panic: runtime error: comparing uncomparable type []int

goroutine 1 [running]:
main.main()
...

The panic message is runtime error: comparing uncomparable type []int. The compiler cannot catch it, because it depends on what is stored in the interface. If the dynamic types differ, the answer is false and no panic occurs, since the comparison stops at the type. The panic only occurs when both hold the same uncomparable type. The same hazard applies to using an interface as a map key, where inserting a slice panics with a related message, and to == on structs with interface fields.

The comparable constraint

Generics added a constraint named comparable for type parameters that need ==, as in a function that finds an element:

main.goGo
package main

import "fmt"

func Index[T comparable](xs []T, target T) int {
	for i, x := range xs {
		if x == target {
			return i
		}
	}
	return -1
}

func main() {
	fmt.Println(Index([]string{"a", "b"}, "b"))
	fmt.Println(Index([]Point{{1, 2}, {3, 4}}, Point{3, 4}))

	mixed := []any{1, "two", 3.0}
	fmt.Println(Index(mixed, any("two")))
}

type Point struct{ X, Y int }
Text
1
1
1

The first two calls are ordinary. The third instantiates T as any, and since Go 1.20 that compiles: interface types satisfy comparable, because they support == at the language level. Before 1.20 it was a compile error, since the constraint meant strictly comparable, types for which == can never panic. The change made the constraint easier to use and it made a promise weaker. A generic function with comparable can now panic at run time if you call it with an interface slice that holds a slice or a map. Go 1.20's release notes explain the decision. If your function must not panic, document it, and recover the cases at the boundary where you build the []any.

Slices, maps and funcs still fail the constraint at compile time, so Index([][]int{...}, ...) is rejected, which is what you want.

Ordered is different from comparable

Whether < works is a separate question. Numbers and strings are ordered. Pointers, channels, structs, arrays, booleans and interfaces are comparable but not ordered. The generic constraint for ordered types is cmp.Ordered, and cmp.Compare gives a three-way comparison that also treats NaN consistently:

main.goGo
package main

import (
	"cmp"
	"fmt"
	"math"
	"slices"
)

func main() {
	fmt.Println(cmp.Compare(1, 2), cmp.Compare("b", "a"), cmp.Compare(2.5, 2.5))
	fmt.Println(cmp.Compare(math.NaN(), 1), cmp.Compare(math.NaN(), math.NaN()))
	fmt.Println(slices.Max([]float64{1.5, 9.25, 3}), min("pear", "apple"))
}
Text
-1 1 0
-1 0
9.25 apple

cmp.Compare returns -1, 0 or 1, and for floats it orders NaN before every other value and equal to itself, which makes sorting floats well defined where < would not. slices.Max and the builtin min use the same ordering. To sort a struct by a field, slices.SortFunc with cmp.Compare on that field is the idiom, as the interfaces guide showed against sort.Interface.

Practical advice

Use comparable types as map keys, and keep key types small and immutable: a string ID, an integer, a small struct of both. Do not use a struct key that contains a pointer unless you mean identity. When you add a field to a key struct, check that it is still comparable, since the compiler will tell you at the map declaration. For interfaces, avoid == unless you know the dynamic types, and prefer a method or a concrete type for equality. For generic code, think about whether comparable really means what you need. If the function must be safe for any caller, take an equality function as a parameter instead, as slices.IndexFunc does. The constraint is defined in the Go 1.20 release notes under the language changes, and the cmp package documents Ordered and Compare.

Also inGenerics
Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.