Why does reflect.DeepEqual report a nil []string and an empty []string{} as unequal?
answer
- length zero is not the whole story
- nilness is part of the value
- three clauses, checked in order
- both nil, or both non-nil
- an absent JSON key versus an empty array
basics
~20 sreflect.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 linesvar a []string // nil
b := []string{} // empty, non-nil
fmt.Println(reflect.DeepEqual(a, b)) // false
fmt.Println(slices.Equal(a, b)) // truego deeper
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.
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.
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.
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