skip to content

Why does slices.Equal treat a nil []string and an empty []string as equal when reflect.DeepEqual does not?

level: middleimportance: should knowfreq 52%

answer

  1. length zero either way
  2. one compares contents, one compares shape
  3. DeepEqual has a both-nil-or-both-non-nil rule
  4. slices.Equal never inspects the backing pointer
  5. maps.Equal splits from DeepEqual the same way

basics

~20 s

slices.Equal compares length and then elements, and both slices have length zero with no elements. reflect.DeepEqual adds a rule that two slices must be both nil or both non-nil, so it reports them as different.

solid answer

~50 s

`slices.Equal` is defined on values: unequal lengths mean false, otherwise elements are compared pairwise with `==` and the first mismatch decides. A nil slice and an empty slice both have length 0 and no elements, so they compare equal. `reflect.DeepEqual` has an extra clause for slices — deeply equal only if both are nil or both are non-nil, plus same length and deeply equal elements — so `reflect.DeepEqual([]string(nil), []string{})` is false. Maps split the same way: `maps.Equal` calls a nil map and an empty map equal, `reflect.DeepEqual` does not. In tests that difference is a steady source of noise, so prefer `slices.Equal` and `maps.Equal` when nil and empty mean the same thing to your code, and assert nil-ness separately when it does not — for instance when the value is about to be encoded, where a nil slice becomes `null` and an empty one becomes `[]`.

code

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

slices.Equal(a, b)    // true:  same length, same (no) elements
reflect.DeepEqual(a, b) // false: one is nil, the other is not

var m map[string]int  // nil map
n := map[string]int{}

maps.Equal(m, n)        // true
reflect.DeepEqual(m, n) // false

go deeper

for a junior

Know that a nil slice and an empty slice both have length 0, and that len, range and append work on both, so most code cannot tell them apart.

for a middle

Put the two definitions side by side: slices.Equal checks length then elements, while reflect.DeepEqual adds a both-nil-or-both-non-nil rule for slices and for maps.

for a senior

Show judgment about where the distinction really matters — an encoded payload, a stored diff, an API response — and make the comparison used in tests match the contract the code promises.

for a principal

Decide for a package other teams import whether an empty result is returned as nil or as an empty slice, document it, and keep every function in the package on the same side of that line.

## Two definitions of "equal" Go gives you more than one way to compare two slices, and they do not agree about the empty case. Both behaviours are correct; they answer different questions. `slices.Equal(s1, s2)` answers **"do these hold the same values?"** Its definition is short: if the lengths differ, return false; otherwise compare elements pairwise in increasing index order with `==` and stop at the first mismatch. It never looks at the capacity, never looks at the backing pointer, and never asks whether either slice is nil. A nil slice has length 0. An empty slice has length 0. Neither has elements. Therefore they are equal: ```go slices.Equal([]string(nil), []string{}) // true ``` `reflect.DeepEqual(x, y)` answers **"are these the same structure?"** and has an extra clause for slices: two slice values are deeply equal when they are both nil or both non-nil, have the same length, and either share the same initial entry (`&x[0] == &y[0]`) or their corresponding elements are deeply equal. The "both nil or both non-nil" test is checked first, so: ```go reflect.DeepEqual([]string(nil), []string{}) // false ``` The map story splits exactly the same way. `maps.Equal(m1, m2)` reports whether the two maps contain the same key/value pairs, and a nil map contains none — so a nil map equals an empty map. `reflect.DeepEqual` again requires both maps to be nil or both non-nil, so it reports `false`. ## Why the distinction exists at all A nil slice is genuinely different from an empty one at the representation level: its data pointer is nil and no array has been allocated. What makes Go pleasant is that almost nothing else cares. `len`, `cap`, `range`, `append`, slicing and every function in the `slices` package accept a nil slice and behave as if it were empty. `var s []T` — the zero value — is nil, and idiomatic Go builds up results by appending to that zero value rather than by calling `make`. Where it stops being invisible is at a boundary that must *serialise* the difference. The obvious one is JSON: a nil slice encodes as `null`, an empty slice as `[]`, and a client parsing your response can absolutely tell. The other is any comparison built on reflection — which is why the distinction is remembered mostly as "the thing that makes my test fail". ## Choosing a comparison The practical rules: 1. **Prefer `slices.Equal` / `maps.Equal`** when nil and empty mean the same thing to your code, which is the common case. They are generic and type-checked at compile time, cost no reflection, and will not fail because a function returned `[]string{}` where the fixture wrote `nil`. 2. **Assert nil-ness separately** when it is part of the contract: `if got != nil { ... }`, or a test on the encoded bytes rather than on the Go value. 3. **Reach for `reflect.DeepEqual`** when the element type is not comparable and you do not want to write a comparison function — but then accept that you are also asserting nil-ness, and say so in the test's failure message so the next person is not confused at 3am. 4. **Use `slices.EqualFunc` / `maps.EqualFunc`** when you want value equality on element types that `==` cannot handle: `slices.Equal` requires a `comparable` element type, so a slice of structs containing a slice field will not compile with it. ## Related gotchas in the same family - **Length first.** `slices.Equal` returns false on differing lengths before comparing anything, so a slice that is a prefix of another is simply not equal. If you want an *ordering* — where the shorter of two slices sorts first when one is a prefix of the other — that is `slices.Compare`, which returns a negative number, zero, or a positive number. - **`==` on slices does not compile.** Slices are not comparable in Go; the only legal comparison is against the literal `nil`. That is precisely why `slices.Equal` had to be written. - **Empty is not always cheap to produce.** `make([]T, 0)` and `[]T{}` both allocate a non-nil header (typically pointing at a shared zero-size location). If a caller distinguishes them, be deliberate about which one your function returns — and be consistent across a package, because a function that returns nil on one path and `[]T{}` on another will make somebody's `reflect.DeepEqual` test flap depending on the input. The summary you can say out loud in an interview: `slices.Equal` compares contents, `reflect.DeepEqual` compares contents **and** shape, and the shape difference between nil and empty is the one Go keeps but most code ignores.

  • Which should a test use to compare two []int results, slices.Equal or reflect.DeepEqual?
    slices.Equal, unless nil-versus-empty is part of what you are asserting. It is type-checked at compile time, does no reflection, and will not fail merely because the function returned an empty slice where the fixture wrote nil. When nil-ness is genuinely the contract, assert it on its own with got == nil.
  • How does slices.Equal handle two slices of different lengths whose common prefix matches?
    It returns false on the length check before looking at any element — equality means same length plus all elements equal. If you want an ordering instead, where a slice that is a prefix of another sorts first, that is slices.Compare, which returns a negative number, zero, or a positive number.
  • Can slices.Equal compare a slice of structs that contain a slice field?
    No, it will not compile: slices.Equal constrains its element type to comparable, and a struct with a slice field is not comparable. Use slices.EqualFunc with your own element comparison, or reflect.DeepEqual if you accept its nil-versus-empty rule and its run-time cost.

saying these in an interview costs you the question

  • Says nil and empty slices are identical in every comparison
  • Blames a flaky test rather than reflect.DeepEqual's nil rule
  • Thinks slices.Equal also compares capacity or the array pointer
  • Assumes maps.Equal and reflect.DeepEqual agree on an empty map
  • Claims a nil slice cannot be passed to slices.Equal