AlgoMaster Logo

Struct Comparison & Equality

Last Updated: May 17, 2026

10 min read

Two Order values look identical on screen, but are they equal as far as Go is concerned? This lesson covers what == and != actually do on a struct, which struct types you're allowed to compare in the first place, why some structs flat-out refuse to be compared, and what your options are when the language won't let you use ==. The rules are short, but they have sharp edges.

What == Does on a Struct

When you compare two struct values with ==, Go compares them field by field. Two struct values are equal if and only if every corresponding field is equal. The comparison runs in source order, stops at the first mismatch, and produces a single bool result. There's no overloading, no custom equality, no method that gets called. It's a direct byte-for-byte comparison of each field's value.

a and b have the same ID, the same Name, and the same Price, so they're equal. a and c differ on ID (1 vs 2), so they're not. The comparison short-circuits the moment it finds a difference; if ID differs, Go never bothers comparing Name or Price. That matters in a moment when we talk about ordering: the field order in the struct definition decides the order of comparison.

You can also compare a struct value to a composite literal directly, which is sometimes useful in tests and switch arms:

The parentheses around the literal are required because of how Go parses the expression. Without them, the compiler gets confused about where the if condition ends and the body begins.

When Is a Struct Comparable?

Here's the rule: a struct type is comparable if and only if every one of its fields has a comparable type. Comparability is not something you opt into; it's a property the compiler infers from the struct's field types.

The comparable types in Go are:

TypeComparable?Notes
bool, all numeric types, stringYesCompared by value
Pointer (*T)YesCompares addresses, not pointed-to values
ChannelYesTwo channel values are equal if they refer to the same channel
InterfaceYes (conditionally)Equal if the dynamic types match and the dynamic values are equal; panics at runtime if the dynamic type is not comparable
Array ([N]T)Yes, if T is comparableElement-by-element comparison
StructYes, if every field type is comparableField-by-field comparison
Slice ([]T)NoCompile error if you use ==
Map (map[K]V)NoCompile error
FunctionNoCompile error

The last three rows are what trip people up. A struct that holds a slice, a map, or a function can never be compared with ==. The compiler enforces this at compile time, not at runtime. That's the good news: you can't ship a binary that crashes because of an uncomparable struct. The compiler refuses to build it.

Order is comparable because every field, including the nested Address, is comparable. Nested structs compose cleanly: Go walks down into ShipTo and compares the address's fields one by one. If any of those nested fields differed, the whole comparison would return false.

When the Compiler Says No

A struct that contains a slice, map, or function is not comparable, and trying to compare two values of that type is a compile-time error. Here's exactly what it looks like.

The error happens at compile time. The program never runs. The message names the offending field type so you can see exactly which field broke comparability. Removing or changing that field is the only way to make == work, and we'll cover what to do when you genuinely need value equality for a cart in a moment.

The same rule applies to maps and functions:

Two compile errors, one per line. The compiler is precise about which field made the type uncomparable, which is helpful when the struct has dozens of fields.

There is one exception worth noting: a struct field of slice, map, or function type can be compared to nil, just not to another value of the same type. That's because == nil on those types is a special form that checks if the header is nil, not a general equality check.

c.Items == nil is fine because it's the "is this slice header nil" check, not a slice-to-slice comparison. The struct itself still can't be compared with ==.

Comparable Structs as Map Keys

If a struct is comparable, you can use it as a map key. The compiler enforces this exactly the same way: only comparable types can be map keys, so the rules line up cleanly. This is one of the most useful consequences of the field-by-field rule.

Two Address literals with the same field values hash to the same key, so the third write finds the existing entry and increments it. The map uses the struct's built-in equality to detect "same key", which is exactly what we want for grouping orders by destination.

The same machinery powers sets-of-structs using map[Struct]bool or map[Struct]struct{}. If you'd ever want a "do I have this thing?" check, this pattern works without any custom code, as long as the struct is comparable.

struct{} is the empty struct, which takes zero bytes. Using it as the map value is the idiomatic way to express "set" in Go.

Pointer Comparison vs Value Comparison

This is the single trap that catches the most people. When you compare two *Product values with ==, Go compares the *pointers*, not the values they point at. Two distinct pointers to two distinct Product values with identical fields are not equal under ==.

p1 and p2 are two different allocations, so their pointer values differ. p1 == p2 reports false even though the products they describe are identical. p3 is the same pointer as p1 (we assigned one to the other), so p1 == p3 is true. To compare the values, you have to dereference both sides with *. *p1 == *p2 reads the struct values and runs the field-by-field comparison.

This shows up in the wild when you've been working with *Product returned from a function or stored in a slice of pointers and you assume two equal-looking products will pass ==. They won't, unless they happen to be the same pointer.

p1 and p3 hold the same address, so p1 == p3 is true. p2 holds a different address, even though the value at that address looks identical to the one p1 points at. p1 == p2 compares the two addresses (0xC000010 vs 0xC000040), gets false, and stops.

A common pattern: write a helper that does the right thing for a given type, and use it instead of == whenever you're working with pointers and you mean "same value":

The helper handles the nil cases up front and dereferences only when both sides are non-nil. Dereferencing a nil pointer would panic, so the nil check is mandatory before the *a == *b.

Different Named Types Don't Compare

Two struct types with identical field names, types, and order are still different types if they have different names. The == operator refuses to compare them. This is true even though Go's structural typing would let you assume otherwise.

The compiler refuses to compare them. The two types are distinct as far as the type system is concerned, even though their layouts are identical. To compare them, you have to convert one to the other first. A conversion is allowed because the underlying field structure matches:

WarehouseAddress(c) converts the CustomerAddress to a WarehouseAddress (same fields, same order, so the conversion is legal), and then == compares two values of the same type. The runtime cost is zero; the conversion is purely a type-system relabel.

If the field names or types or order differed even slightly, the conversion would not compile. The point of all this strictness is to make sure you don't accidentally compare two semantically different things that happen to share a layout.

reflect.DeepEqual for Uncomparable Types

When == won't do, the standard library's escape hatch is reflect.DeepEqual. It compares two values recursively, looking inside slices, maps, and structs, and produces a single bool. It does not care about comparability rules; it works on any types.

reflect.DeepEqual walks a and b element by element, finds them equal, and returns true. For a vs c, the slices differ in order, so the result is false. The function handles nil-vs-empty distinctions, nested maps, pointers (following them and comparing what they point at), and a few edge cases worth reading about in the documentation.

The tradeoffs of reflect.DeepEqual:

Aspect==reflect.DeepEqual
SpeedFast, often a single CPU compareSlow, uses reflection and recurses
Compile-time safetyYes, refuses to compile on uncomparable typesNo, takes any and runs at runtime
Works on slices, maps, functionsNoYes
NaN behaviorNaN != NaNSame: NaN != NaN (so two structs with NaN fields are not "deeply equal")
Type-checked at compile timeYesNo, type mismatch returns false at runtime

reflect.DeepEqual is right for tests and one-off comparisons where you really do want "same shape, same values". It is wrong for hot paths, and it is wrong as the default tool for production equality. The general advice is: use it where you'd otherwise have to write a recursive comparison by hand, and reach for a custom Equal method for everything else.

When You Need a Custom Equal Method

The right fix for an uncomparable struct in production code is usually a method named Equal on the type. When == doesn't work and you want something better than reflect.DeepEqual, write a function (or method) that knows how to compare your specific struct.

The method handles the slice comparison explicitly. It's faster than reflect.DeepEqual, type-checked at compile time, and lets you define what equality actually means for your domain. Maybe you want to ignore item order, or treat names case-insensitively, or normalize whitespace. A custom Equal is the place to put that logic, and Go's standard library uses this pattern in places like time.Time.Equal and bytes.Equal.

The NaN Gotcha

Floats have one more wrinkle worth flagging. The IEEE 754 standard says that NaN (not-a-number) is not equal to anything, including itself. Go follows that rule. A struct that contains a float64 set to NaN will fail == against an otherwise identical struct.

a and b have the same byte pattern, but the == on the Price fields returns false (because NaN != NaN), which makes the whole struct comparison false. This is an edge case, but it shows up if you're doing analytics or any computation that can produce NaN. If NaN is a real possibility in your data, you can't rely on == for value equality and you have to write the comparison yourself, treating NaN however your domain wants.

Common Mistakes

A few patterns that come up over and over.

Assuming `==` works on a struct that holds a slice. It won't compile. The fix is either to drop the slice from the equality, key off a different field, or write an Equal method.

Comparing two pointers when you meant to compare values. p1 == p2 is rarely what you want unless you're doing identity checks (like comparing against a sentinel). For value equality, dereference: *p1 == *p2. And remember to nil-check first.

Comparing across named struct types. Two structs with identical layouts but different names are different types, and == refuses to compare them. Convert one to the other if the conversion is meaningful, or rethink why you have two types with the same layout.

Forgetting NaN. If your struct has a float64 field that can hold computation results, == may unexpectedly return false for two structs that look identical. Decide whether your domain treats NaN == NaN and write the comparison accordingly.

Reaching for `reflect.DeepEqual` everywhere. It's a fine tool for tests and one-off comparisons. It is not the default. Custom Equal methods are faster, clearer, and type-checked. Keep reflect.DeepEqual for the cases where it genuinely earns its overhead.

Summary

  • == on structs runs a field-by-field comparison and short-circuits at the first mismatch. The field order in the struct definition decides the order of comparison.
  • A struct is comparable if and only if every field type is comparable. Slices, maps, and functions are not comparable, so any struct containing them is not comparable either.
  • Trying to compare an uncomparable struct is a compile-time error, not a runtime panic. The compiler names the offending field type to help locate it.
  • Comparable structs can be used as map keys. This is the foundation of "set of struct" and "group by struct" patterns.
  • Comparing pointers with == compares addresses, not values. Use *p1 == *p2 for value equality, with a nil check first.
  • Differently-named structs with identical layouts are still different types and won't compare directly. Convert one to the other when the conversion is meaningful.
  • reflect.DeepEqual is the escape hatch for uncomparable types in tests and one-off code, but it's slower than == and has its own edge cases (nil vs empty slice, NaN). For production code, prefer a typed Equal method.
  • NaN floats break struct equality. Two byte-identical structs containing NaN values will fail ==. Handle this explicitly if your data can produce NaN.

The next lesson covers JSON with Structs, which is where struct tags from earlier finally pay off: encoding and decoding JSON to and from Go struct values, customizing field names, and handling missing or optional fields cleanly.