Struct Comparability
Understand when and how Go structs can be compared for equality, including the critical rule that all fields must be comparable types
In Go, you compare two struct values of the same type with == and != only when every field inside the struct has a comparable type. This single rule decides whether a struct is "comparable" in the language sense.
Comparable fields are ones where Go already knows how to answer "are these two values equal?" without ambiguity — things like numbers, strings, booleans, pointers, channels, and arrays of comparable types. Structs themselves are comparable if all their fields are comparable, which means nested structs follow the same rule recursively.
Non-comparable types break this chain. A struct that contains a slice, a map, or a function is not comparable, and you cannot use == on it at all.
Why this matters:
Struct comparability is not just a trivia item. It decides whether you can use a struct as a map key, whether you can check if a value equals its zero value with ==, and whether the compiler will let you write a direct equality test in a conditional.
When a struct is comparable
A struct is comparable if all its field types are comparable. For example:
type Point struct {
X, Y int
}
type LabeledPoint struct {
Point
Label string
}
Here, Point has two int fields — comparable. LabeledPoint has an embedded Point (a comparable struct) and a string — also comparable.
You can freely compare instances:
p1 := LabeledPoint{Point{1, 2}, "start"}
p2 := LabeledPoint{Point{1, 2}, "start"}
fmt.Println(p1 == p2) // true
The comparison checks every field, recursively, in their declaration order. If all corresponding fields are equal, the structs are equal. No hidden rules — just a simple field-by-field walk.
Works with arrays and pointers too:
Arrays of comparable types are comparable, so a struct with a fixed-size array field stays comparable. Pointers are comparable; the equality compares the addresses, not the pointed-to values — unless you need deep pointer comparison, in which case you move to reflect.DeepEqual.
The compile-time blocker: uncomparable fields
If your struct contains a slice, map, or function field, the compiler will stop you the moment you write a == b. It does not silently fall back to something else; it produces an error like:
invalid operation: a == b (struct containing []string cannot be compared)
Consider a Favorites struct with a slice:
type Favorites struct {
Color string
Hobbies []string
}
a := Favorites{Color: "blue", Hobbies: []string{"reading"}}
b := Favorites{Color: "blue", Hobbies: []string{"reading"}}
// a == b // compile error: struct containing []string cannot be compared
This example shows the mismatch: the Color field is comparable, but the Hobbies slice makes the entire struct uncomparable. The rule is all-or-nothing.
The same happens with maps and functions. Even an empty slice (nil) inside the struct disqualifies it from ==. There is no exception for nil slices — the field's type is what matters.
Cannot use as map keys:
An uncomparable struct cannot be used as a map key. If you need a map key containing slice-like data, convert the slice to a string (like joining with a delimiter) or use a comparable wrapper type.
Deep equality with reflect.DeepEqual
When you must compare structs that contain uncomparable fields, the reflect package provides DeepEqual. It walks the entire structure — slices, maps, pointers, nested structs — and checks whether the actual data is equal, not whether the memory layout is identical.
import "reflect"
type Favorites struct {
Color string
Hobbies []string
}
a := Favorites{Color: "blue", Hobbies: []string{"reading"}}
b := Favorites{Color: "blue", Hobbies: []string{"reading"}}
fmt.Println(reflect.DeepEqual(a, b)) // true
DeepEqual handles slices by comparing length and each element. For maps, it compares key sets and corresponding values. For pointers, it follows them and compares the underlying values.
DeepEqual is powerful but has costs:
DeepEqual uses reflection, so it's slower than == and has no compile-time safety — it will silently return false for unexported fields that == would ignore (since == only works on exported fields). Use it sparingly, and prefer designing structs to be comparable when possible.
DeepEqual respects unexported fields:
Unlike ==, which requires struct fields to be exported for comparison, DeepEqual can see and compare unexported fields via reflection. This can be useful in testing but surprising if you expect the same behavior as ==.
Comparing against the zero value
A struct’s zero value (all fields set to their zero) can only be compared with == if the struct is comparable. For a struct with a slice field, you can’t write p.Favorites == Favorites{} — the compiler rejects it. To check whether a struct with uncomparable fields is at its zero state, you must either:
- Use
reflect.DeepEqualagainst the zero value. - Write a dedicated method that checks each field individually, handling slices by length or content as needed.
type Person struct {
Name string
Favorites Favorites // uncomparable because of Hobbies slice
}
p := Person{Name: "Joe"}
if reflect.DeepEqual(p.Favorites, Favorites{}) {
fmt.Println("Favorites not set")
}
The alternative, writing a custom IsZero method, gives you full control and avoids reflection:
func (f Favorites) IsZero() bool {
return f.Color == "" && f.Lunch == "" && f.Place == "" && len(f.Hobbies) == 0
}
This is more explicit and keeps the check fast.
Common pitfalls
Several misunderstandings trip up newcomers — and even experienced developers — around struct comparability.
Mistaking field-by-field comparison for deep comparison:
When a struct is comparable, == compares the values of pointer fields, not the data they point to. Two pointer fields are equal only if they point to the same address, not if the contents behind them are identical. If you need deep pointer equality, use reflect.DeepEqual.
Forgetting that copying doesn't create a comparable struct:
Copying a struct with an uncomparable field does not make the copy comparable. The type remains the same, and the == operator still refuses to compile.
Expecting that nil slices allow comparison:
A nil slice is still a slice. A struct field of type []string makes the struct uncomparable, even if its runtime value is nil. The compiler only looks at the type.
You can still compare exported fields individually:
When a struct as a whole is uncomparable, nothing stops you from comparing its individual comparable fields directly. This is often cleaner than reaching for DeepEqual when only a subset of fields matters.
Summary
Struct comparability is determined entirely by the field types: if every field is comparable, the struct is comparable, and ==/!= work. If any field contains a slice, map, or function, the compiler rejects the equality operators.
This rule is the foundation for using structs as map keys, writing equality checks, and reasoning about value semantics. When you need to compare structs that can't meet this rule, reflect.DeepEqual is the escape hatch — powerful, but slower and lacking compile-time protection.