A Go test asserting reflect.DeepEqual(got, []string{}) fails because got is a nil slice - how do you fix it?
answer
- both have length zero, both print []
- one has a backing array, one does not
- DeepEqual checks nil-ness before elements
- decide what the function promises
- %v hides it, %#v shows it
basics
~20 sreflect.DeepEqual reports false for a nil slice against an empty non-nil one. Decide what the function promises, then either write want to match it, compare with slices.Equal, which treats both as length zero, or assert len(got) == 0.
solid answer
~40 s`reflect.DeepEqual` distinguishes a nil slice from an empty non-nil slice: both have length 0, but one has no backing array, so it reports false. The fix is not to reach for a looser comparison first - it is to decide what the function promises. Callers can observe the difference: `encoding/json` marshals a nil slice as `null` and an empty one as `[]`, so a library's wire format really does change. Pin that promise in the test by writing `want` as `nil` or as `[]string{}` deliberately. If the distinction is genuinely irrelevant to callers, say so and use `slices.Equal`, which reports true for any two length-zero slices, or simply assert `len(got) == 0`. Note that `%v` prints `[]` for both, so the failure reads `got [], want []` until you switch to `%#v`.
code
go · 6 linesvar got []string // nil: length 0, no backing array
empty := []string{} // non-nil: length 0
fmt.Println(reflect.DeepEqual(got, empty)) // false
fmt.Println(slices.Equal(got, empty)) // true
fmt.Println(got == nil, empty == nil) // true falsego deeper
Remember the outcome: reflect.DeepEqual reports false for a nil slice against an empty one, even though both have length zero and both print as []. Knowing the surprise exists is most of the value here.
Explain the mechanism - one slice has a backing array and the other does not - and name the three fixes: match want deliberately, compare with slices.Equal, or assert the length.
Settle the contract rather than the assertion. Say whether callers can observe the difference, with JSON encoding as the obvious place, and make the test pin whichever answer the library actually promises.
The call you own is what a shared library returns for 'nothing' and whether its docs say so. Changing it later flips null to [] for every consumer, which is a breaking change nobody will have tested for.
## Two values that print the same and are not equal A slice value carries a pointer to a backing array, a length and a capacity. `var got []string` leaves all three at their zero values: it is the **nil slice**. `[]string{}` allocates a zero-length array and points at it: it is an **empty non-nil slice**. Both have length 0. Both are safe to `range` over, to call `len` on, and to `append` to. Both print as `[]` under `%v`. `reflect.DeepEqual` does not treat them as equal. Its rule for slices is: they are deeply equal when they are both nil or both non-nil, have the same length, and either share the same first element pointer or have deeply equal elements. "Both nil or both non-nil" is the clause that bites - a nil slice compared with `[]string{}` is false before the elements are ever considered. So the assertion fails, the message reads `got [], want []`, and the test author concludes something is broken in the tooling. Nothing is broken. The test asked a question it did not mean to ask. ## Fix the contract first, then the assertion The useful question is not "how do I make this comparison pass" but **"which one does this function promise?"** For a library other teams import, that promise is real and observable: - `encoding/json` marshals a nil slice field as `null` and an empty non-nil slice as `[]`. A consumer decoding your response in another language sees a different document. - A caller writing `if lines == nil` gets a different answer from a caller writing `if len(lines) == 0`. - A function that returns `nil, nil` for "no results" and one that returns an allocated empty slice are making different statements about whether the absence is meaningful. Once you have decided, the test should **state the decision**, not paper over it. If the function promises nil for "no line items", write `var want []string` - now the test fails loudly if someone later changes the function to allocate, which is exactly the regression you want to catch. If it promises an allocated empty slice, write `[]string{}` and the same protection applies in the other direction. ## When the distinction genuinely does not matter Sometimes it truly is internal - a helper returning a working set that never crosses a package boundary. Then say so and pick a comparison that means what you want: - `slices.Equal(got, want)` reports true whenever both slices have the same length and equal elements, so any two length-zero slices are equal regardless of nil-ness. `maps.Equal` behaves the same way for maps. - `if len(got) != 0` is the bluntest and clearest form when "empty" is the whole assertion. What you should not do is edit the production function to allocate an empty slice purely so one test goes green. That changes the library's observable output - including its JSON - to satisfy an assertion nobody thought about. ## Reading the failure message This is the case that makes `%#v` earn its keep: ``` t.Errorf("Lines() = %v, want %v", got, want) // Lines() = [], want [] t.Errorf("Lines() = %#v, want %#v", got, want) // Lines() = []string(nil), want []string{} ``` The `%v` form is actively misleading - it shows two identical-looking values next to the word `want`, which reads like a tooling bug. The `%#v` form prints the Go-syntax representation, `[]string(nil)` against `[]string{}`, and the diagnosis takes two seconds. When a comparison fails on values that print the same, reach for `%#v` before you reach for anything else. ## Maps behave the same way A nil map and an empty non-nil map are likewise not deeply equal, print the same as `map[]`, and differ in JSON (`null` against `{}`). The one extra wrinkle is that reading from a nil map is fine and writing to one panics, so a function returning a nil map is handing callers something more fragile than an empty one - a reason to prefer allocating in that case even though the slice answer often goes the other way. ## What a reviewer should ask When a pull request changes an assertion from `reflect.DeepEqual` to `slices.Equal`, or edits `want` from `nil` to `[]string{}`, the question is always the same: **did the contract change, or did the test just stop asking?** Both are legitimate outcomes; only one of them should be silent.
- Does the nil-versus-empty difference ever reach a caller of the library?Yes, in the two places that matter most. `encoding/json` marshals a nil slice as `null` and an empty non-nil slice as `[]`, so consumers decoding your output in any language see a different document. And a caller who writes `if result == nil` gets a different answer from one who writes `if len(result) == 0`. Everything else - `len`, `range`, `append` - behaves identically on both.
- Does the same trap apply to maps?It does: a nil map and an empty non-nil map are not deeply equal, both print as `map[]`, and they marshal as `null` against `{}`. `maps.Equal` treats both as empty. The extra difference is that reading a missing key from a nil map is fine while writing to one panics, so returning a nil map hands callers something more fragile than returning an allocated empty one.
- A teammate fixes this by making the function allocate an empty slice. Is that acceptable?Only if it is a deliberate contract change, discussed as one. Allocating changes the library's JSON output from `null` to `[]` for every consumer, which is a breaking change for anyone with a strict decoder or a schema. If the intent really is "the test should not care", the change belongs in the test - `slices.Equal` or a length check - not in the production return value.
An empty envelope and no envelope at all both contain nothing, but only one of them arrived in the post - and the recipient can tell.
saying these in an interview costs you the question
- Says a nil slice and an empty slice are the same thing
- Changes production code to allocate an empty slice so one test goes green
- Reads got [], want [] and concludes the test tooling is broken
- Claims reflect.DeepEqual ignores nil-ness for slices and maps
- Assumes callers cannot observe the difference in JSON output