Skip to content
gohacks.dev
Basics

Comparing Structs Slices and Maps in Go the Right Way

Use == for comparable structs, slices.Equal and maps.Equal for collections, and cmp.Diff in tests. The nil versus empty trap is explained.

6 min read
Summarize in AI
Flat illustration of an editor window full of code.

Go gives you == for the simple cases and nothing for the rest, and that is deliberate. Whether two slices are "equal" depends on whether you mean the same elements, the same memory or the same elements in any order, and the language will not guess. What you get instead is a short list of tools, each with a clear meaning. This how-to puts them in order of how often I reach for them: the operator, the slices and maps helpers, a hand-written Equal method and the test-time comparison libraries. It builds on the structs guide and comparable types, and it is part of the Go basics path.

Tested with Go 1.25.6, github.com/google/go-cmp v0.7.0, macOS on Apple Silicon (arm64)

Structs with ==

Two struct values are equal if all their corresponding fields are equal. That works when every field has a type that supports ==:

main.goGo
package main

import "fmt"

type Point struct{ X, Y int }

type Segment struct {
	From, To Point
	Label    string
}

func main() {
	a := Segment{Point{0, 0}, Point{3, 4}, "ab"}
	b := Segment{Point{0, 0}, Point{3, 4}, "ab"}
	c := a
	c.To.Y = 5

	fmt.Println(a == b, a == c, a != c)

	seen := map[Point]bool{{1, 2}: true}
	fmt.Println(seen[Point{1, 2}], seen[Point{2, 1}])
}
Text
true false true
true false

Nested structs and arrays compare field by field, and the same equality is what lets a Point act as a map key. Floats compare with the usual IEEE rules, including that NaN is not equal to itself, so a struct containing a NaN is not equal to its own copy. Pointers compare by address, not contents, so two structs holding different pointers to equal values are different. When a field is a slice, map or function, the compiler refuses the comparison, with a message that names the field, as the structs article shows.

Slices and maps with the standard library

For slices and maps of comparable elements, slices.Equal and maps.Equal compare contents:

main.goGo
package main

import (
	"bytes"
	"fmt"
	"maps"
	"slices"
	"strings"
)

func main() {
	a := []int{1, 2, 3}
	b := []int{1, 2, 3}
	fmt.Println(slices.Equal(a, b), slices.Equal(a, []int{3, 2, 1}))

	var nilSlice []int
	empty := []int{}
	fmt.Println(slices.Equal(nilSlice, empty))

	m1 := map[string]int{"x": 1, "y": 2}
	m2 := map[string]int{"y": 2, "x": 1}
	fmt.Println(maps.Equal(m1, m2))

	fmt.Println(bytes.Equal([]byte("go"), []byte("go")))

	fmt.Println(slices.EqualFunc([]string{"Go", "RUST"}, []string{"go", "rust"}, strings.EqualFold))
}
Text
true false
true
true
true
true

Order matters for slices: [1 2 3] and [3 2 1] are different, and for maps it does not, since a map has no order. A nil slice and an empty slice compare equal under slices.Equal, which is usually what you want, and is different from reflect.DeepEqual, below. bytes.Equal is the same for byte slices and is what you use instead of converting to strings. slices.EqualFunc takes your own comparison function for elements that are not comparable with == or that need a looser notion of equal, as with the case-insensitive comparison above. For slices of structs that contain slices, none of the helpers compose, and you need the next section.

An Equal method for your own types

When "equal" has a domain meaning, write it down. A type with a slice field, a floating point tolerance or a private cache has no useful ==, and a method is better than every caller inventing a comparison:

main.goGo
package main

import (
	"fmt"
	"math"
	"slices"
)

type Reading struct {
	Sensor string
	Values []float64
	cache  string
}

func (r Reading) Equal(o Reading) bool {
	return r.Sensor == o.Sensor &&
		slices.EqualFunc(r.Values, o.Values, func(a, b float64) bool {
			return math.Abs(a-b) < 1e-9
		})
}

func main() {
	a := Reading{"t1", []float64{0.1 + 0.2, 1.5}, "x"}
	b := Reading{"t1", []float64{0.3, 1.5}, "y"}
	fmt.Println(a.Equal(b))
}
Text
true

The method ignores the private cache field, compares the float slices with a tolerance and uses the Sensor string exactly. The name Equal with that signature is also a convention that go-cmp honors, so test failures use it automatically. If you give a type an Equal method, say in its doc comment which fields participate. Note the floats again: 0.1 + 0.2 is not 0.3 exactly, as the floating point article shows, so a tolerance is what makes this equal.

Comparing in tests

In a test you want two things: a quick yes or no, and a readable explanation when the answer is no. The standard library's reflect.DeepEqual gives the first. cmp.Diff from github.com/google/go-cmp gives both:

main.goGo
package main

import (
	"fmt"
	"reflect"

	"github.com/google/go-cmp/cmp"
)

type Profile struct {
	Name string
	Tags []string
	Meta map[string]int
}

func main() {
	want := Profile{"ada", []string{"a", "b"}, map[string]int{"n": 1}}
	got := Profile{"ada", []string{"a", "c"}, map[string]int{"n": 2}}

	fmt.Println(reflect.DeepEqual(want, got))
	fmt.Println(cmp.Diff(want, got))

	var nilTags, emptyTags []string = nil, []string{}
	fmt.Println(reflect.DeepEqual(nilTags, emptyTags), cmp.Equal(nilTags, emptyTags))
}
Text
false
  main.Profile{
  	Name: "ada",
  	Tags: []string{
  		"a",
- 		"b",
+ 		"c",
  	},
- 	Meta: map[string]int{"n": 1},
+ 	Meta: map[string]int{"n": 2},
  }

false false

DeepEqual returns false and nothing else. cmp.Diff prints a diff in which - marks the wanted value and + the actual one, line by line, down to the slice element and the map key that differ. That difference is the reason to use it in every test that compares more than a scalar. The last line is the trap that sends people to this article: DeepEqual says a nil slice and an empty slice are different, and cmp.Equal says the same by default, until you pass cmpopts.EquateEmpty(). Neither is wrong, and they mean different things, so decide whether your API promises nil or empty and write the expectation accordingly. For JSON responses this matters, because nil marshals as null and empty as [].

The standard library has no cmp.Diff. The built-in cmp package, which has Compare and Less, is unrelated and orders values. If a test compares times, use Equal on the time.Time values or the cmpopts option for time tolerance, since == on times is unreliable, as time and durations explains.

Interface values and errors

Interface values compare with == when their dynamic types are comparable: equal if both hold the same type and equal values. If the dynamic type is a slice or map, the comparison panics at run time instead of failing to compile, as the empty interface article demonstrates. For errors, the question is rarely "are these two values identical" and nearly always "is this error, or something it wraps, the one I expect". Use errors.Is for a sentinel and errors.As for a type. A comparison like err == ErrNotFound stops working the first time a layer wraps the error with %w, which is a quiet regression to avoid.

Performance and where DeepEqual is the wrong tool

reflect.DeepEqual walks values through reflection, and it is slow compared with a typed comparison. It is fine in tests and wrong in a hot path. If you compare large structures in production, write Equal methods, or hash a canonical form and compare hashes if exactness is acceptable. Do not use DeepEqual on values containing functions (it returns false for any non-nil function), on cyclic structures (it handles cycles but check the cost), or on types with unexported state that should not matter. Errors are a special case: compare them with errors.Is and errors.As, never == against a freshly built error. And for sets, compare by converting both to a map or sorting both, since order should not matter, using slices.Sort first.

What to do with the result

Pick the tool by the question. Is this the same value, as a number or a key? Use ==. Do these slices or maps hold the same things? Use slices.Equal or maps.Equal. Does this complex struct match my expectation in a test? Use cmp.Diff. Is this my domain object equal to that one under my rules? Write the method. Reach for reflect.DeepEqual last, and when you do, remember the nil-versus-empty difference. The language's rules are in the specification on comparison operators, and the slices.Equal and go-cmp docs define the helpers precisely.

Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.