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.

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:
| Type | Equal when |
|---|---|
| Boolean, integer, float, complex | The values are equal (NaN is never equal to anything) |
| String | The bytes are identical |
| Pointer | Both point at the same variable, or both are nil |
| Channel | Both were created by the same make, or both are nil |
| Array | All elements are equal, and the element type is comparable |
| Struct | All non-blank fields are equal, and every field type is comparable |
| Interface | Same dynamic type and equal dynamic values |
Three types are missing, and for them the only comparison allowed is with nil:
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)
}# 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:
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)))
}true true true
false true true
false true
true false
false true
false true falseStrings 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:
package main
import "fmt"
func main() {
var a, b any = []int{1}, []int{1}
fmt.Println(a == b)
}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:
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 }1
1
1The 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:
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"))
}-1 1 0
-1 0
9.25 applecmp.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.


