What is the difference between a nil slice and an empty non-nil slice in Go?
answer
- both are empty, only one is nil
- the header's data pointer is the difference
- len and range cannot tell them apart
- a JSON consumer can
- test length, not nilness
basics
~20 sBoth have length and capacity 0, and both work with len, range and append. The differences are that only the nil slice equals nil, and that encoding/json writes a nil slice as null and an empty non-nil slice as [].
solid answer
~50 sA slice value is a header of pointer, length and capacity. `var s []int` is nil: all three fields are zero. `[]int{}` and `make([]int, 0)` are non-nil headers with length 0. Everything you normally do treats them the same - `len` and `cap` are 0 for both, `for range` runs zero iterations over both, `append` works on both, and indexing panics on both. Only three things see a difference: `s == nil`, which is true for one and false for the other; `encoding/json`, which marshals nil as `null` and empty as `[]`; and `reflect.DeepEqual`, which reports them unequal. The idiom is to return a nil slice for "no results" and to have callers test `len(s) == 0`, so nobody depends on which one they got. Return `[]T{}` deliberately only when a JSON consumer must see `[]` rather than `null`.
code
go · 11 linesvar a []int // nil slice
b := []int{} // empty, non-nil
fmt.Println(len(a), len(b)) // prints: 0 0
fmt.Println(a == nil, b == nil) // prints: true false
for range a {
fmt.Println("never runs")
}
a = append(a, 1) // appending to a nil slice is finego deeper
Be ready to say that both are empty and that append, len and range work on a nil slice, so var s []T is a perfectly good starting point for building a list.
Explain the slice header - pointer, length, capacity - and name the three places the difference is observable: comparison to nil, JSON output, and reflect.DeepEqual in tests.
Show the caller-facing rule: document "may be empty", standardise on len(s) == 0, and decide nil-versus-empty explicitly at the boundary where the value is serialised, so a client never sees null one release and [] the next.
Own it as an API contract. Whether an exported endpoint or library function emits null or [] for an empty list is a compatibility promise, so pin it once, test it, and do not let it drift with an internal refactor.
## Both are empty; only one is nil A slice value in Go is a small **header**: a pointer to a backing array, a length, and a capacity. A **nil slice** is the zero value of a slice type - all three fields zero, including the data pointer. An **empty non-nil slice** is a header whose data pointer is non-nil (it points at a zero-length array or, for `[]T{}`, at a shared placeholder) with length 0. ```go var a []int // nil slice: a == nil is true b := []int{} // empty, non-nil: b == nil is false c := make([]int, 0) // also empty and non-nil ``` ## Everything you normally do treats them identically - `len(a)` and `len(b)` are both `0`; `cap` is `0` for both. - `for i, v := range a` runs zero iterations, exactly like ranging over `b`. Ranging over a nil slice is **not** a panic and does not need a guard. - `append(a, x)` works: append allocates a backing array when the capacity is insufficient, and a nil slice simply has zero capacity. This is why the idiomatic accumulator is `var out []T` and not `out := []T{}`. - `copy(dst, a)` copies `min(len(dst), len(a))` elements, which is 0. - Indexing panics on both: `a[0]` and `b[0]` are both an out-of-range panic. ## Where the difference is observable 1. **The comparison itself.** `a == nil` is true, `b == nil` is false. This is the only comparison a slice supports - slice values cannot be compared to each other with `==`. 2. **Serialisation.** `encoding/json` marshals a nil slice as `null` and an empty non-nil slice as `[]`. If a client distinguishes "field absent / unknown" from "field present and empty", the difference in the JSON is load-bearing, and it is decided by which slice value you produced. 3. **Reflection-based equality.** `reflect.DeepEqual(a, b)` reports false for a nil and an empty slice of the same type, which surprises people in tests. Comparing lengths, or comparing with a helper that treats both as empty, avoids the trap. 4. **Round-tripping.** Unmarshalling `{"tags": null}` or a JSON document with the field missing leaves the field as a nil slice; unmarshalling `{"tags": []}` gives an empty non-nil slice. That is how "absent" and "explicitly empty" survive a round trip if you want them to. ## Which one should your code produce? The community default: **return a nil slice for "no results"** and let callers use `len(s) == 0`. It costs no allocation, it is the zero value, and it composes with `append`. Go's own standard library follows this - functions that find nothing typically return a nil slice, not an allocated empty one. Deliberately return `[]T{}` in exactly two situations: - The value is going to be serialised and the consumer must see `[]` rather than `null` (a JSON API where an empty array is part of the contract, and clients would otherwise have to handle `null`). - A test or an equality check based on `reflect.DeepEqual` compares against `[]T{}` - although the better fix there is usually to compare lengths or to normalise before comparing. ## The rule for callers **Never make a caller care.** A function that returns a slice should document "may be empty", and every caller should test `len(s) == 0`. Writing `if s != nil` as an emptiness test is a defect waiting for the day the function starts returning `[]T{}`; writing `if s == nil` as a "no data" test is a defect waiting for the day it returns an allocated empty slice. `len(s) == 0` is correct in both worlds, and it is also correct for a nil map. ## A note on the same question for maps The same split exists for maps - a nil map and an empty `map[K]V{}` both have `len` 0 and both range zero times - with one hard difference: writing to the nil one stops the program, while writing to the empty one is fine. So "nil is as good as empty" is true for slices and only half true for maps.
- Which should a function return when it finds no results?A nil slice, in almost every case. It is the zero value, it costs no allocation, and `append` and `range` work on it, so callers written against `len(s) == 0` never notice. Return an allocated `[]T{}` only when the value is serialised and the consumer's contract requires `[]` instead of `null`.
- Why does reflect.DeepEqual report a nil slice and an empty slice as unequal?DeepEqual compares nilness explicitly for slices and maps before comparing elements, so it treats "no backing array" and "a zero-length backing array" as different values. In tests, either compare lengths, or normalise one side before comparing, rather than asserting on whichever form the code happens to produce today.
- Does the same nil-versus-empty distinction matter for maps?The read side matches: a nil map and an empty map both have `len` 0 and both range zero times, and `encoding/json` writes `null` versus `{}`. The write side does not: writing a key into an empty map is fine, while writing into a nil map stops the program. So a nil map is only as good as an empty one if nothing writes to it.
saying these in an interview costs you the question
- Says ranging over a nil slice panics
- Says append requires a make'd slice
- Uses s != nil as the emptiness test
- Claims the two slices compare equal with ==
- Thinks encoding/json emits [] for a nil slice