Question

How to Compare Structs, Slices, and Maps for Equality in Go

gogo-reflect

Question

I want to check whether two structs, slices, and maps are equal in Go.

However, I am running into problems with the following code:

package main

import (
    "fmt"
    "reflect"
)

type T struct {
    X int
    Y string
    Z []int
    M map[string]int
}

func main() {
    t1 := T{
        X: 1,
        Y: "lei",
        Z: []int{1, 2, 3},
        M: map[string]int{
            "a": 1,
            "b": 2,
        },
    }

    t2 := T{
        X: 1,
        Y: "lei",
        Z: []int{1, 2, 3},
        M: map[string]int{
            "a": 1,
            "b": 2,
        },
    }

    fmt.Println(t2 == t1)
    // error: invalid operation: t2 == t1 (struct containing []int cannot be compared)

    fmt.Println(reflect.ValueOf(t2) == reflect.ValueOf(t1))
    // false

    fmt.Println(reflect.TypeOf(t2) == reflect.TypeOf(t1))
    // true

    a1 := []int{1, 2, 3, 4}
    a2 := []int{1, 2, 3, 4}

    fmt.Println(a1 == a2)
    // invalid operation: a1 == a2 (slice can only be compared to nil)

    m1 := map[string]int{
        "a": 1,
        "b": 2,
    }
    m2 := map[string]int{
        "a": 1,
        "b": 2,
    }

    fmt.Println(m1 == m2)
    // invalid operation: m1 == m2 (map can only be compared to nil)
}

Why does this happen, and what is the correct way to compare two structs, slices, or maps for equality in Go?

Short Answer

By the end of this page, you will understand how equality works in Go, why some values can be compared with == and others cannot, and how to correctly compare structs, slices, and maps. You will also learn when to use reflect.DeepEqual and when writing your own comparison logic is a better choice.

Concept

In Go, not every type supports comparison with ==.

A value is comparable only if Go defines == for that type. This matters because Go treats equality as a language rule, not as something automatically inferred from the contents of a value.

What can be compared with ==

These types are comparable:

  • booleans
  • numbers
  • strings
  • pointers
  • channels
  • interfaces, if their dynamic values are comparable
  • arrays, if their element type is comparable
  • structs, if all fields are comparable

What cannot be compared with ==

These types are not comparable to each other with ==:

  • slices
  • maps
  • functions

They can only be compared to nil.

Why your struct comparison fails

This struct:

type T struct {
    X int
    Y string
    Z []int
    M map[string]int
}

contains a slice field Z and a map field M. Since slices and maps are not comparable, the struct itself is also not comparable with ==.

Why reflect.ValueOf(t2) == reflect.ValueOf(t1) is false

reflect.ValueOf returns reflect.Value objects. Comparing those with == compares the reflect.Value wrappers, not the original data deeply. That is not the same as checking whether the underlying structs contain equal data.

Why reflect.TypeOf(t2) == reflect.TypeOf(t1) is true

reflect.TypeOf returns the type metadata. Both values have the same type T, so the comparison is true.

The usual solution: reflect.DeepEqual

When you want to compare the contents of structs, slices, maps, arrays, and other nested values, the common standard-library tool is:

reflect.DeepEqual(a, b)

It checks whether two values are deeply equal by recursively comparing their contents.

Example:

fmt.Println(reflect.DeepEqual(t1, t2)) // true
fmt.Println(reflect.DeepEqual(a1, a2)) // true
fmt.Println(reflect.DeepEqual(m1, m2)) // true

Why this matters in real programs

Equality checks are used everywhere:

  • testing expected vs actual results
  • comparing configuration objects
  • detecting changes in data
  • validating API responses
  • deciding whether to update cached state

Knowing whether == checks value equality, identity, or is not allowed at all helps you avoid compiler errors and subtle bugs.

Mental Model

Think of Go values as falling into two groups:

  • simple boxes: values Go knows how to compare directly with ==
  • containers with internal storage: values like slices and maps that point to underlying data

A slice is not the data itself. It is more like a small label that describes:

  • where the data starts
  • how long it is
  • how much capacity it has

A map is also a runtime-managed structure, not just a plain value with obvious byte-for-byte equality.

So when you write ==, Go asks:

Is this type one of the kinds I know how to compare directly?

If yes, it compares. If no, the code does not compile.

For deep comparison, imagine opening both containers and checking their contents item by item. That is what reflect.DeepEqual does.

Take Quiz

Syntax and Examples

Basic rules

// Comparable values
fmt.Println(10 == 10)         // true
fmt.Println("go" == "go")   // true

// Not allowed
// []int{1,2} == []int{1,2}   // compile error
// map[string]int{"a":1} == map[string]int{"a":1} // compile error

Comparing structs with ==

A struct can be compared only if all of its fields are comparable.

package main

import "fmt"

type Point struct {
    X int
    Y int
}

func main() {
    p1 := Point{X: 1, Y: 2}
    p2 := Point{X: 1, Y: 2}

    fmt.Println(p1 == p2) // true
}

This works because both X and Y are int, and int is comparable.

Comparing structs, slices, and maps with reflect.DeepEqual

package main

import (
    "fmt"
    "reflect"
)

type T struct {
    X int
    Y string
    Z []int
    M map[string]int
}

func main() {
    t1 := T{
        X: 1,
        Y: "lei",
        Z: []int{1, 2, 3},
        M: map[string]int{"a": 1, "b": 2},
    }

    t2 := T{
        X: 1,
        Y: "lei",
        Z: []int{1, 2, 3},
        M: map[string]int{"a": 1, "b": 2},
    }

    a1 := []int{1, 2, 3, 4}
    a2 := []int{1, 2, 3, 4}

    m1 := map[string]int{"a": 1, "b": 2}
    m2 := map[string]int{"a": 1, "b": 2}

    fmt.Println(reflect.DeepEqual(t1, t2)) // true
    fmt.Println(reflect.DeepEqual(a1, a2)) // true
    fmt.Println(reflect.DeepEqual(m1, m2)) // true
}

Writing your own comparison logic

Sometimes you should compare fields manually instead of using reflection.

package main

import "fmt"

type User struct {
    ID   int
    Name string
}

func equalUser(a, b User) bool {
    return a.ID == b.ID && a.Name == b.Name
}

func main() {
    u1 := User{ID: 1, Name: "Ana"}
    u2 := User{ID: 1, Name: "Ana"}

    fmt.Println(equalUser(u1, u2)) // true
}

This is often clearer, faster, and safer when you know exactly which fields matter.

Step by Step Execution

Consider this example:

package main

import (
    "fmt"
    "reflect"
)

func main() {
    a := []int{1, 2, 3}
    b := []int{1, 2, 3}

    fmt.Println(reflect.DeepEqual(a, b))
}

Here is what happens step by step:

  1. a := []int{1, 2, 3} creates a slice containing three integers.
  2. b := []int{1, 2, 3} creates another slice with the same values.
  3. a == b would not compile, because slices cannot be compared with == except against nil.
  4. reflect.DeepEqual(a, b) inspects both values.
  5. It sees that both values are slices.
  6. It checks whether they have the same length.
  7. It compares each element in order:
    • 1 vs 1
    • 2 vs 2
    • 3 vs 3
  8. Since all elements match, the result is true.
  9. fmt.Println(...) prints true.

Now compare that with this example:

fmt.Println(reflect.ValueOf(a) == reflect.ValueOf(b))

That does not do a deep content comparison. It compares two reflect.Value objects, which is a different thing entirely. So it is not the right tool for checking whether two slices contain the same data.

Real World Use Cases

1. Comparing API responses in tests

When you call an API and decode JSON into Go structs, you often want to verify that the actual response matches the expected response.

if !reflect.DeepEqual(got, want) {
    t.Errorf("unexpected response")
}

2. Detecting configuration changes

Suppose your application loads a config file into a struct. You may want to reload the app only if the new config is different from the old one.

3. Comparing cached data

A service may skip expensive work if a newly computed result is equal to the cached version.

4. Data processing pipelines

When processing records, you may compare maps or slices to check whether data has changed after a transformation step.

5. Validating input normalization

If your code sorts, cleans, or deduplicates input, you may compare the original and transformed results to determine whether a change occurred.

Take Quiz

Real Codebase Usage

In real Go projects, developers often avoid relying on generic deep comparison everywhere.

Common patterns

Guard clauses

Return early when values are obviously different.

func sameLength(a, b []int) bool {
    if len(a) != len(b) {
        return false
    }
    return true
}

Manual comparison for important domain types

If only specific fields matter, compare those directly.

func sameUser(a, b User) bool {
    return a.ID == b.ID && a.Email == b.Email
}

This avoids accidental comparison of fields that should not affect equality.

Validation before comparison

Sometimes developers normalize data first.

  • trim spaces
  • sort slices if order should not matter
  • fill default values

Then compare the normalized values.

Error handling and testing

In tests, reflect.DeepEqual is common for quick assertions. In production logic, manual comparisons are often preferred because they are explicit.

Comparing slices or maps by business rules

For example:

  • order may matter for a slice in one feature
  • order may not matter in another feature
  • some map keys may be ignored

In those cases, write custom comparison functions instead of assuming generic deep equality matches your business rules.

Common Mistakes

Mistake 1: Using == on slices or maps

Broken code:

a1 := []int{1, 2, 3}
a2 := []int{1, 2, 3}
fmt.Println(a1 == a2)

Why it fails:

  • slices cannot be compared with == except to nil

Use instead:

fmt.Println(reflect.DeepEqual(a1, a2))

Mistake 2: Assuming a struct is always comparable

Broken code:

type T struct {
    Numbers []int
}

fmt.Println(T{Numbers: []int{1}} == T{Numbers: []int{1}})

Why it fails:

  • a struct is comparable only if all its fields are comparable

Mistake 3: Comparing reflect.Value instead of underlying data

Broken idea:

reflect.ValueOf(t1) == reflect.ValueOf(t2)

Why it is wrong:

  • this compares reflect.Value wrappers, not the contents deeply

Use:

reflect.DeepEqual(t1, t2)

Mistake 4: Forgetting that nil and empty values can differ

Example:

var a []int = nil
b := []int{}

fmt.Println(reflect.DeepEqual(a, b)) // false

Why this surprises beginners:

  • both may look “empty”, but one is nil and the other is an allocated empty slice

Mistake 5: Using deep equality when business equality is different

Example:

// These may be considered equal by your app logic
// if order does not matter, but DeepEqual will say false.
a := []int{1, 2, 3}
b := []int{3, 2, 1}

Avoid this by defining equality based on your actual requirements.

Comparisons

ApproachWorks forDeep content checkNotes
==Comparable types onlyNoFast and simple, but not allowed for slices and maps
reflect.DeepEqualStructs, slices, maps, arrays, nested valuesYesConvenient, especially in tests
Manual comparison functionAny typeYes, if you implement itBest when only some fields matter or custom rules apply

== vs reflect.DeepEqual

Feature==reflect.DeepEqual
Compiler supportBuilt into the languageFunction from reflect
Works on slicesNoYes
Works on mapsNoYes
Works on structs with slice/map fieldsNoYes
Compares type onlyNoNo
Compares nested contentsNoYes

reflect.TypeOf vs reflect.ValueOf vs reflect.DeepEqual

FunctionWhat it gives youGood for equality?
reflect.TypeOf(x)The type of xOnly checks whether types match
reflect.ValueOf(x)A reflection wrapper around the valueNot for deep equality by itself
reflect.DeepEqual(a, b)A deep comparison resultYes

When to choose each

  • Use == for simple comparable values.
  • Use reflect.DeepEqual for quick deep checks, especially in tests.
  • Use manual comparison when the definition of “equal” depends on business rules.
Take Quiz

Cheat Sheet

Quick rules

  • == works only on comparable types.
  • slices, maps, and functions are not comparable with ==.
  • slices and maps can only be compared to nil.
  • a struct is comparable only if every field is comparable.
  • an array is comparable if its element type is comparable.
  • use reflect.DeepEqual(a, b) for deep comparison of structs, slices, and maps.

Common examples

fmt.Println(1 == 1)                    // true
fmt.Println("a" == "a")              // true
fmt.Println([2]int{1, 2} == [2]int{1, 2}) // true
// invalid
// []int{1,2} == []int{1,2}
// map[string]int{"a":1} == map[string]int{"a":1}

Deep equality

reflect.DeepEqual([]int{1, 2}, []int{1, 2}) // true
reflect.DeepEqual(
    map[string]int{"a": 1},
    map[string]int{"a": 1},
) // true

Important edge case

var a []int = nil
b := []int{}

reflect.DeepEqual(a, b) // false

Practical advice

  • Prefer == for simple types.
  • Prefer manual comparison for domain-specific rules.
  • Use reflect.DeepEqual when you need generic deep comparison.

FAQ

Why can't slices be compared with == in Go?

Slices are not directly comparable in Go because they are descriptors for underlying data, not plain comparable values. You can only compare a slice to nil.

How do I compare two slices in Go?

A common general approach is reflect.DeepEqual(a, b). If you need custom rules, write your own comparison function.

How do I compare two maps in Go?

Use reflect.DeepEqual(m1, m2) for a deep content comparison, or loop through keys and values manually if you need custom behavior.

Can I compare two structs with ==?

Yes, but only if all fields inside the struct are comparable. If the struct contains a slice or map, == will not compile.

What is the difference between reflect.TypeOf and reflect.DeepEqual?

reflect.TypeOf checks the type metadata. reflect.DeepEqual checks whether two values contain equal data.

Is reflect.ValueOf(a) == reflect.ValueOf(b) a valid deep comparison?

No. That compares reflect.Value objects, not the underlying values deeply.

Is nil slice equal to an empty slice in Go?

Not with reflect.DeepEqual. A nil slice and an empty non-nil slice are different values.

Should I always use reflect.DeepEqual?

No. It is useful, especially in tests, but manual comparison is often better in production code when equality should follow business rules.

Take Quiz

Mini Project

Description

Build a small Go program that compares two configuration objects and reports whether they are equal. This demonstrates how equality works for structs containing slices and maps, which cannot be compared directly with ==.

Goal

Create a program that compares nested config data correctly and prints whether the configs match.

Requirements

  • Define a Config struct that contains simple fields, a slice field, and a map field.
  • Create two config values with the same contents.
  • Show that == cannot be used directly on the struct.
  • Use reflect.DeepEqual to compare the configs.
  • Change one nested value and show that the comparison becomes false.
Take Quiz

Keep learning

Related questions

View more