DH
4 min read

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.

golangtesting

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:

a := 42
b := 42
fmt.Println(a == b) // true

x := "hello"
y := "hello"
fmt.Println(x == y) // true

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:

type Point struct {
X, Y int
}

p1 := Point{1, 2}
p2 := Point{1, 2}
fmt.Println(p1 == p2) // true

Add a slice or map field, and the struct becomes non-comparable:

type Config struct {
Tags []string // slice field — struct is now 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:

a := Point{1, 2}
b := Point{1, 2}

pa := &a
pb := &b

fmt.Println(pa == pb) // false — different addresses
fmt.Println(*pa == *pb) // true — same values

Dereference deliberately.


Comparing Interfaces

Two interface values are equal if they have the same dynamic type and equal dynamic values:

var i1 interface{} = 42
var i2 interface{} = 42
fmt.Println(i1 == i2) // true

var i3 interface{} = []int{1, 2}
var i4 interface{} = []int{1, 2}
// fmt.Println(i3 == i4) — runtime panic: comparing uncomparable type

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:

import "reflect"

a := []int{1, 2, 3}
b := []int{1, 2, 3}
fmt.Println(reflect.DeepEqual(a, b)) // true

m1 := map[string]int{"x": 1}
m2 := map[string]int{"x": 1}
fmt.Println(reflect.DeepEqual(m1, m2)) // true

Caveats:

  • Slower than direct comparison — avoid in hot paths.
  • Distinguishes between nil slices and empty slices ([]int(nil) vs []int{}), causing false negatives in tests.
  • Treats NaN != NaN for 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:

import "cmp"

fmt.Println(cmp.Compare(3, 5)) // -1
fmt.Println(cmp.Compare(5, 5)) // 0
fmt.Println(cmp.Compare(7, 5)) // 1

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:

type Money struct {
Amount int64
Currency string
}

func (m Money) Equal(other Money) bool {
return m.Amount == other.Amount && m.Currency == other.Currency
}

This pattern makes intent explicit, is testable, and doesn't rely on field ordering or reflection.


Practical Rules

  1. Use == for primitives, comparable structs, and pointers-by-address.
  2. Use reflect.DeepEqual in tests only; be aware of nil/empty slice gotchas.
  3. Never compare interface values holding non-comparable types — guard with type assertions first.
  4. Define .Equal() methods on your domain types.
  5. Use cmp.Compare for 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

Damian Hodgkiss

Senior Staff Engineer at Sumo Group, leading development of AppSumo marketplace. Technical solopreneur with 25+ years of experience building SaaS products.

Creating Freedom

Join me on the journey from engineer to solopreneur. Learn how to build profitable SaaS products while keeping your technical edge.

    Proven strategies

    Learn the counterintuitive ways to find and validate SaaS ideas

    Technical insights

    From choosing tech stacks to building your MVP efficiently

    Founder mindset

    Transform from engineer to entrepreneur with practical steps