skip to content

Comparability and Map Keys

== works on structs and arrays whose elements are comparable but never on a slice, map or func, and two interfaces holding uncomparable values panic at run time instead of failing to compile.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

4

Which Go types can be compared with `==`, and which ones fail to compile?

level: juniorimportance: must knowfreq 74%

answer

  1. the compiler decides, not the values
  2. three kinds are left out
  3. slice, map, func: only against nil
  4. structs and arrays inherit it from their parts
  5. same rule decides valid map keys

basics

~20 s

Booleans, numbers, strings, pointers, channels, interfaces, and structs or arrays built entirely from comparable parts support ==. Slices, maps and functions do not: the compiler rejects ==, and the only equality they allow is a comparison against nil.

solid answer

~40 s

Go's spec makes booleans, numeric types, strings, pointers, channels and interface types comparable, plus structs and arrays whose parts are all comparable. Slices, maps and function values are not comparable: writing `a == b` on them is a compile-time error, and the single exception is comparing them with `nil`. Comparability is a property of the *type*, not of the values, so it is settled by the compiler — a struct with one `[]byte` field can never be compared, whatever that field happens to hold. When you need element-wise equality for the three uncomparable kinds you call a function instead: `slices.Equal`, `maps.Equal`, `bytes.Equal`, or `reflect.DeepEqual` as a slow general fallback. The same rule decides map keys, which is why `map[[]string]int` does not compile while `map[[2]string]int` does.

code

go · 8 lines
go
type Point struct{ X, Y int }
type Path struct{ Pts []Point }

func demo(a, b Point, p, q Path) {
	_ = a == b // ok: every field of Point is comparable
	_ = p == q // compile error: Path has a slice field
	_ = p.Pts == nil // ok: a slice may only be compared with nil
}

go deeper

for a junior

Be ready to list the comparable kinds and name the three that are not: slices, maps and functions. Remember that those three may still be compared with nil.

for a middle

Explain that comparability is a compile-time property of the type and is inherited recursively by structs and arrays, and say what == actually compares for pointers, strings and channels.

for a senior

Show the production consequence: an API that takes any or exposes a struct as a map key inherits the rule, and adding one slice field to a shared struct breaks every == and every map key downstream.

for a principal

Own the guidance that a type meant to be a key or a set element keeps all of its fields comparable, and that element-wise equality belongs in an explicit Equal helper rather than being smuggled into ==

## The rule Go defines equality on types, not on values. The language spec lists exactly which types are **comparable** — meaning `==` and `!=` are legal on them: - **Boolean** values. - **Numeric** values (integers, floats, complex), compared by numeric value. - **String** values, compared byte by byte. - **Pointer** values, equal when they hold the same address (or are both nil). - **Channel** values, equal when they came from the same `make` call (or are both nil). - **Interface** values, equal when their dynamic types are identical and their dynamic values are equal. - **Struct** types, comparable if *every* field type is comparable. - **Array** types, comparable if the element type is comparable; two arrays are equal when all corresponding elements are equal. Three kinds are deliberately left out: - **Slices** - **Maps** - **Function values** For those, `x == y` is a **compile-time error**, and the only equality expression allowed is `x == nil`. ## Why the three are excluded Each of the excluded kinds has no obvious, cheap definition of equality. Should two slices be equal when they share a backing array, or when their elements match? The first is surprising and the second is O(n) — and Go's `==` is meant to be a cheap, constant-shaped operation you can also use to hash a map key. Maps have the same problem plus unordered contents. Function values have no meaningful identity at all once the compiler is free to merge, inline or wrap them; a closure has captured state that nothing can inspect. Rather than pick a debatable answer, Go removes the operator and makes you say what you meant. Because the rule is about types, it is checked once at compile time and it is **transitive**. A struct is comparable only if all of its fields are, applied recursively: ```go type Inner struct{ A int } // comparable type Outer struct{ I Inner } // comparable type Broken struct{ M map[string]int } // NOT comparable type AlsoBroken struct{ B Broken } // NOT comparable, by inheritance ``` No runtime state can rescue `Broken`: even if the map field is nil, `Broken{} == Broken{}` does not compile. ## What comparison actually compares Beginners are often surprised by *which* thing is compared: - Two **pointers** are equal when they hold the same address. Two pointers to two distinct variables that happen to hold identical structs are **not** equal; `*p == *q` compares the pointed-to values instead. - Two **strings** are compared by content, not by data pointer, so `s == "ok"` is content equality. - Two **arrays** are compared element by element and copy by value, which is why `[16]byte` is a fine map key while `[]byte` is not. - Two **structs** are compared field by field, including unexported fields you cannot see from another package. - Two **interface** values compare the dynamic type first, then the dynamic value. ## What to do instead for the uncomparable three ```go slices.Equal(a, b) // element-wise for []T where T is comparable maps.Equal(m1, m2) // same keys, equal values bytes.Equal(p, q) // the fast path for []byte reflect.DeepEqual(x, y) // general, reflective, slow — a last resort ``` `reflect.DeepEqual` is not what `==` does under the hood; it is a separate reflective walk with its own rules (it treats a nil slice and an empty non-nil slice as different, for instance). Reach for the typed helpers first, keep `reflect.DeepEqual` for tests and unusual shapes. Function values have no equality helper at all — if you need to know "is this the same handler I registered?", store an identifier alongside it rather than trying to compare the function. ## Why this matters beyond `==` The comparable/not-comparable split is the same rule that decides what may be a **map key** and what may be an element of a set-shaped map. `map[K]V` requires `K` to be comparable, so the compiler rejects `map[[]string]int` with `invalid map key type`. It is also the rule behind interface equality panics: an interface type is statically comparable, but if the dynamic type it holds turns out to be a slice, map or function, the comparison fails at run time instead. That is the one place where the compile-time guarantee does not hold, and it is worth knowing before you build an API whose key type is `any`. ## Quick self-check Given `type S struct{ Name string; Tags []string }` — can you use `S` as a map key? No: the `Tags` field is a slice, so `S` is not comparable, and no value of `S` ever will be.

  • If slices are not comparable, how do you check whether two `[]string` values hold the same elements?
    Call `slices.Equal(a, b)`, which walks both slices and compares element by element; for `[]byte` use `bytes.Equal`, which is faster. `reflect.DeepEqual` also works but is reflective and slow, and it has its own rules — it reports a nil slice and an empty non-nil slice as different. Plain `==` is only ever legal against `nil`.
  • Are two Go pointers equal when they point at distinct variables holding the same value?
    No. Pointer equality compares addresses, so `p == q` is true only when both point at the same variable, or both are nil. To compare the pointed-to values you dereference: `*p == *q`, which is then subject to the ordinary comparability rules for that type.
  • What does `==` on two Go channel values compare?
    Channel identity. Two channel values are equal when they were produced by the same `make` call, or when both are nil. It says nothing about buffered contents, capacity or direction, so `==` on channels answers "is this the same channel?", never "do these carry the same data?".

Comparability is like whether a shape has a barcode printed on it: the printing is decided when the type is designed, not when the box is filled.

saying these in an interview costs you the question

  • Says == on two slices compares their elements
  • Thinks reflect.DeepEqual is what == does under the hood
  • Believes comparability depends on the values, not on the type
  • Claims a struct with a map field compares fine when the map is nil
  • Uses == on []byte instead of bytes.Equal
open as a page

What must be true for two Go interface values to be equal with `==`?

level: middleimportance: should knowfreq 46%

basics

~20 s

Two interface values are equal only when both hold nothing at all, or when their dynamic types are identical and their dynamic values are equal. An any holding int(1) never equals an any holding int64(1).

open as a page

What makes a Go struct type usable as a map key, and what disqualifies it?

level: middleimportance: should knowfreq 58%

basics

~20 s

A struct can key a Go map only if every field type is comparable, applied recursively. One slice, map or function field disqualifies it at compile time; replace that field with an array, a string or another canonicalised value.

open as a page

Why does a Go `map[any]bool` compile fine but panic when some keys are inserted?

level: seniorimportance: nice to knowfreq 33%

basics

~20 s

The static key type any is comparable, so the map compiles. At run time the map must hash the key's dynamic type, and a slice, map or function value cannot be hashed, so the runtime panics naming that type.

open as a page