skip to content

Deep Equality Semantics

reflect.DeepEqual walks two values structurally, and its edge rules — a nil slice never equals an empty one, func values never match, unexported fields count — are why a test passes but is wrong.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

4

Why does reflect.DeepEqual report a nil []string and an empty []string{} as unequal?

level: juniorimportance: must knowfreq 55%

answer

  1. length zero is not the whole story
  2. nilness is part of the value
  3. three clauses, checked in order
  4. both nil, or both non-nil
  5. an absent JSON key versus an empty array

basics

~20 s

reflect.DeepEqual requires two slices to be both nil or both non-nil before it compares length and elements. A nil slice and an empty slice fail that first check, so it reports false even though both have length zero.

solid answer

~40 s

`reflect.DeepEqual` treats nilness as part of a slice's value. Its rule for slices is: both nil or both non-nil, then same length, then equal elements. `var a []string` is nil and `[]string{}` is not, so the comparison stops at the first clause and returns false, even though `len` is 0 for both. The identical rule applies to maps. This bites most often after a JSON round trip: a missing key leaves the field nil while `"items": []` produces an allocated empty slice, so a decoded value and a hand-built expected value disagree for a reason nothing in the data shows. If you want the two treated as the same, compare with `slices.Equal` or `maps.Equal`, which look only at length and contents, or normalise one side before comparing.

code

go · 5 lines
go
var a []string  // nil
b := []string{} // empty, non-nil

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

go deeper

for a junior

Be ready to say that a nil slice and an empty slice both have length zero but are different values, and that reflect.DeepEqual checks nilness before it checks anything else.

for a middle

Explain the three ordered clauses for slices and maps, and name where the two forms come from in practice: an unappended var declaration, and an absent JSON key versus an explicit empty array.

for a senior

Show the judgment call: decide whether nilness carries meaning in your domain, then either normalise it away at the boundary or assert on it deliberately rather than patching each failing comparison.

for a principal

Own the contract question. If your API's JSON emits null for absent and [] for empty, that difference is now part of what consumers depend on, and changing it later is a breaking change no compiler will catch.

## The rule `reflect.DeepEqual(x, y any) bool` is a recursive relaxation of Go's `==`. It walks two values and, kind by kind, decides equality. For slices its documented rule has three clauses, checked in order: 1. the two slices are **both nil or both non-nil**; 2. they have the **same length**; 3. they **share the same backing array start**, or their elements up to that length are deeply equal. A nil slice and an empty non-nil slice pass clause 2 (both are length 0) but fail clause 1, so the answer is `false`. Maps have the same shape of rule: both nil or both non-nil, same length, then the same map object or matching keys mapping to deeply equal values. This is deliberate, not an oversight. In Go a slice value is a three-word header — a data pointer, a length and a capacity. A nil slice has a nil data pointer; `[]string{}` has a non-nil pointer to a zero-length allocation. They are genuinely different values, and `reflect.DeepEqual` is designed to report structural identity rather than "behaves the same in a loop". ## Where the two forms come from Almost every codebase produces both without intending to: - `var out []string` followed by a filter loop that never appends leaves `out` nil; `out := make([]string, 0)` or `[]string{}` leaves it empty and non-nil. Both are idiomatic, and `append` works on either. - `encoding/json` decoding leaves a field untouched when the key is absent, so it keeps the zero value, nil. A key whose value is `[]` makes the decoder allocate, giving an empty non-nil slice. - Encoding back out is asymmetric in the same way: a nil slice marshals to `null`, an empty non-nil slice marshals to `[]`. - A function that returns `nil, err` on the error path and an allocated slice on the success path mixes the two by design. So a comparison between a value that came off the wire and a value written by hand in the test can differ purely in nilness while every element matches. ## Everything else about nilness stays the same The confusing part is that the two forms are interchangeable nearly everywhere else in the language. `len` and `cap` are 0 for both. Ranging over either yields nothing. `append` on a nil slice allocates and works. Indexing either panics. Only three things distinguish them: comparison against `nil`, the JSON encoding, and `reflect.DeepEqual`. That is exactly why this shows up as a surprise rather than as a bug people anticipate. ## What to do about it There are three honest responses, and which one is right depends on whether nilness is meaningful in your domain. **Compare with something that ignores nilness.** `slices.Equal(a, b)` compares length and then elements with `==`; it reports a nil slice and an empty slice as equal. `maps.Equal` does the same for maps. These are also faster and type-checked at compile time, because they use type parameters rather than runtime reflection. **Normalise before comparing.** If the difference is noise, force one representation at the boundary — for example, ensure a constructor always allocates, or convert nil to empty when decoding. This is worth doing when the value crosses a serialisation boundary in both directions and the `null`-versus-`[]` distinction confuses consumers. **Keep the distinction and assert on it.** Sometimes nil genuinely means "absent" and empty means "present but empty" — a patch document is the classic case. Then `reflect.DeepEqual`'s strictness is the feature, and the fix is to make the expected value nil on purpose. ## The related trap `reflect.DeepEqual` also returns false for values of different dynamic types, so `int32(1)` and `int64(1)` are not deeply equal, and neither are a `[]string` and an `[3]string` array. And because the function takes two `any` parameters, passing an untyped `nil` on one side and a typed nil pointer on the other gives false: the first argument is a nil interface, the second is an interface holding a nil `*T`, which is not the same interface value. Both arguments being untyped `nil` returns true. The one-line summary worth memorising: for slices and maps, `reflect.DeepEqual` asks "same nilness, same length, same contents" — in that order, and it stops at the first no.

  • Does the same nil-versus-empty rule apply to maps?
    Yes. A map comparison is both nil or both non-nil, then same length, then the same map object or every key mapping to a deeply equal value. So a nil map and `map[string]int{}` are not deeply equal, exactly as with slices.
  • How would you compare two slices while treating nil and empty as the same?
    Use `slices.Equal`, which compares length and then elements and never looks at nilness; `maps.Equal` does the same for maps. Alternatively normalise at the boundary so one representation is produced consistently, then the question stops arising.
  • What JSON does a nil slice produce compared with an empty one?
    A nil slice marshals to `null`; an empty non-nil slice marshals to `[]`. That asymmetry is why a decode-then-encode round trip can change a payload, and why teams that care about the wire format normalise nil to empty before encoding.

saying these in an interview costs you the question

  • Says DeepEqual only looks at length and elements
  • Claims a nil slice and an empty slice are interchangeable everywhere
  • Thinks len(s) == 0 proves s is nil
  • Says a nil slice marshals to [] like an empty one
  • Assumes appending to a nil slice fails
open as a page

Why can reflect.DeepEqual(x, x) return false for some values of x?

level: middleimportance: should knowfreq 38%

basics

~20 s

reflect.DeepEqual is not reflexive. A float NaN anywhere in the value fails the == that DeepEqual falls back to for numbers, and a non-nil func value is deeply equal to nothing at all, itself included.

open as a page

reflect.DeepEqual compares unexported fields too — how can that make a golden comparison fail only in CI?

level: seniorimportance: should knowfreq 40%

basics

~20 s

reflect.DeepEqual compares every field, exported and unexported, including state you never set: a time.Time's monotonic clock reading and location pointer, or a lazily filled cache. When the CI machine's zone, clock or environment differs, those hidden fields differ and the comparison fails.

open as a page

How does reflect.DeepEqual terminate on a cyclic structure instead of recursing forever?

level: middleimportance: nice to knowfreq 24%

basics

~20 s

reflect.DeepEqual records each pair of addresses it is already comparing and returns true when it meets the same pair again, so a cycle terminates instead of recursing forever. Identical pointers, map objects and slice backing arrays short-circuit the same way.

open as a page