Right, let’s settle this. You’ve probably seen both nil and empty slices in the wild and maybe even used them interchangeably. That works… until it spectacularly doesn’t. The difference is one of the most beautifully subtle, yet profoundly important, distinctions in Go. It’s the difference between having absolutely nothing (nil), and having something that happens to contain nothing (an empty slice).

Think of it this way: a nil slice is like having a blank check. You haven’t committed to any specific bank account (backing array), and the check’s “amount” field (length and capacity) is zero. An empty slice is like writing a check for $0.00 from your very real, but currently empty, checking account. The effect of trying to spend that money is the same (you get nothing), but the underlying financial reality is different.

Here’s the canonical representation. A nil slice looks like this in its runtime header:

var nilSlice []string // nil slice
// Slice header: {
//   Pointer: nil,
//   Length:  0,
//   Capacity: 0,
// }

An empty slice, created with a slice literal, looks like this:

emptySlice := []string{} // empty, non-nil slice
// Slice header: {
//   Pointer: 0xSOME_REAL_MEMORY_ADDRESS, // Points to a zero-length memory location
//   Length:  0,
//   Capacity: 0,
// }

You can also create one using make with a zero length:

alsoEmpty := make([]string, 0)

The Practical Sameness (Most of the Time)

Here’s why you can often get away with ignoring the difference: the language designers were smart and made the len(), cap(), and for range operations work identically on both. The following code runs without a hitch and produces the exact same output for both slices.

fmt.Println(len(nilSlice))      // 0
fmt.Println(cap(emptySlice))    // 0
for i, v := range nilSlice {    // Loop never runs; no iteration.
    fmt.Println(i, v)
}

append is also brilliantly designed to handle nil slices perfectly. Appending to a nil slice allocates a new backing array, just as it would for any slice with insufficient capacity.

nilSlice = append(nilSlice, "I'm no longer nil!")
emptySlice = append(emptySlice, "Me too!")
fmt.Println(nilSlice) // ["I'm no longer nil!"]
fmt.Println(emptySlice) // ["Me too!"]

After that append, both slices are now functionally identical. This elegance is why you see return nil all over the standard library instead of return []string{}.

The Critical Difference (When It Matters)

So if they behave the same, why should you care? You care in the few, crucial places where the value of the slice header itself is inspected, rather than the data it points to. The most common place is when marshaling to JSON.

This is the classic “gotcha”. A nil slice marshals to JSON null, while an empty slice marshals to an empty array [].

var nilSlice []string
emptySlice := []string{}

nilJSON, _ := json.Marshal(nilSlice)
emptyJSON, _ := json.Marshal(emptySlice)

fmt.Println(string(nilJSON))   // null
fmt.Println(string(emptyJSON)) // []

If you’re building a REST API, returning null for a field versus [] can break client applications that are poorly written to expect one or the other. It’s a distinction without a difference to Go, but a massive difference to a JavaScript client.

So, Which One Should You Use?

This isn’t just academic; it’s a matter of semantics and clarity.

  • Favor nil for representational correctness. A nil slice represents the absence of a value. It’s the zero value for slices. If your function returns a slice of results and there are none, return nil is the most idiomatic and memory-efficient choice. It says, “I have no results to give you.” This is the overwhelmingly common case.

  • Use an empty slice when emptiness is meaningful data. This is rarer. You’d do this if you need to explicitly signal to a receiver (especially across a serialization boundary like JSON) that a list exists but is currently empty. It says, “Here is the list of results; it contains zero items.”

The best practice? Be intentional. Don’t just use []string{} because you saw it in a tutorial. Default to nil for the zero value, and only reach for the empty slice literal when you have a specific reason to distinguish between “absolutely nothing” and “an empty collection.” Your code will be more idiomatic, and you’ll avoid those weird JSON serialization bugs that have you scratching your head at 2 a.m.