Golang Compare: A Practical Guide to Equality, Ordering, and Deep Inspection
Master Go's comparison semantics: when == works, why slices and maps don't, and production-tested patterns for safe equality checks across types.
Golang Compare: A Practical Guide to Equality, Ordering, and Deep Inspection
After more than 25 years building backend systems, comparison bugs are among the most insidious production issues you'll encounter. They're quiet, they're contextual, and Go's type system makes them both safer and more surprising than most languages you've probably worked with.
Why Comparison in Go Is Not as Simple as ==
Go gives you a statically typed, compiled language where the rules for comparing values are strict and deliberate. If you're coming from Python or JavaScript — where == does whatever it feels like — you'll hit unexpected behaviour quickly.
The core distinction:
- Comparable types support
==and!=directly. - Non-comparable types (slices, maps, functions) do not.
- Ordered types support
<,>,<=,>=.
The compiler enforces this immediately. That's a feature.
Comparing Primitive Types
For integers, floats, strings, and booleans, == works as expected:
Strings in Go are compared by value, not reference. No .equals() needed. This catches out Java and Python developers.
Comparing Structs
Structs are comparable if all their fields are comparable types. Go compares them field by field:
Add a slice or map field, and the struct becomes non-comparable:
== will not compile. Respect that.
Comparing Pointers
Pointer comparison with == checks whether both pointers point to the same memory address, not whether the values at those addresses are equal:
Dereference deliberately.
Comparing Interfaces
Two interface values are equal if they have the same dynamic type and equal dynamic values:
That last example compiles but panics at runtime. This is a genuine danger: comparing interface{} (or any) values that might hold slices or maps will crash. Guard with type assertions first.
Deep Comparison with reflect.DeepEqual
When you need to compare slices, maps, or nested structs, reflect.DeepEqual is the standard-library option:
Caveats:
- Slower than direct comparison — avoid in hot paths.
- Distinguishes between
nilslices and empty slices ([]int(nil)vs[]int{}), causing false negatives in tests. - Treats
NaN != NaNfor floats (IEEE 754 compliant, but often unexpected).
Use it in tests, not production logic where performance matters.
Ordering with the cmp Package and Generics
Since Go 1.21, the cmp package provides a clean Compare function for ordered types:
For sorting and ordering custom types, implement the cmp.Ordered constraint or provide a comparison function. This replaces a lot of the boilerplate that used to require manual sort.Interface implementations.
Writing a Custom Equal Method
For domain types with complex equality rules, define your own method:
This pattern makes intent explicit, is testable, and doesn't rely on field ordering or reflection.
Practical Rules
- Use
==for primitives, comparable structs, and pointers-by-address. - Use
reflect.DeepEqualin tests only; be aware of nil/empty slice gotchas. - Never compare interface values holding non-comparable types — guard with type assertions first.
- Define
.Equal()methods on your domain types. - Use
cmp.Comparefor ordered comparisons in generic or sorting contexts.
Go's comparison model rewards deliberateness. Once you internalise these rules, you'll write clearer, more predictable code and spend less time chasing equality bugs.
Damian Hodgkiss
Senior Staff Engineer at Sumo Group, leading development of AppSumo marketplace. Technical solopreneur with 25+ years of experience building SaaS products.