Empty Slice in Go: Proper Initialization with Examples

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Empty Slice in Go: How to Avoid Mistakes from the Start

\n\n

Every Go developer sooner or later faces the question: "how to properly initialize an empty slice?". It seems like a simple task, but it's not so straightforward. In the official Go FAQ and on Stack Overflow, hundreds of comments and thousands of votes are dedicated to this topic. Why?

\n\n

Because in Go there are two fundamentally different ways to create an empty slice, and the choice between them affects performance, code readability, and even behavior during serialization. If you write in Go and want to write clean, efficient code without surprises — this article is for you.

\n\n

We will cover:

\n
    \n
  • How var s []int differs from s := make([]int, 0)
  • \n
  • When to use a nil slice and when to use an empty slice
  • \n
  • How this affects JSON, memory, and performance
  • \n
  • What Go gurus and standard library authors say
  • \n
\n\n

Two Siblings: nil slice vs empty slice

\n\n

In Go, a slice is a structure of three fields: a pointer to an array, length, and capacity. When we talk about an empty slice, we mean a slice with a length of 0. But how we create it matters.

\n\n

Method 1: var declaration (nil slice)

\n\n
var s []int  // s == nil, len(s) == 0, cap(s) == 0
\n\n

This is a nil slice. It does not point to any array in memory. However, you can safely call append on it — Go will allocate memory automatically.

\n\n

Method 2: make initialization (empty slice)

\n\n
s := make([]int, 0)  // s != nil, len(s) == 0, cap(s) == 0
\n\n

This is an empty slice. It already points to a zero-length array (a special zerobase in the Go runtime).

\n\n

At first glance, the difference is unnoticeable. But let's dig deeper.

\n\n

Console Output and JSON — The First Pitfall

\n\n

Try running this code:

\n\n
package main\n\nimport (\n    "encoding/json"\n    "fmt"\n)\n\nfunc main() {\n    var nilSlice []int\n    emptySlice := make([]int, 0)\n\n    fmt.Printf("nilSlice: %v, is nil: %v\\n", nilSlice, nilSlice == nil)\n    fmt.Printf("emptySlice: %v, is nil: %v\\n", emptySlice, emptySlice == nil)\n\n    // JSON serialization\n    nilJSON, _ := json.Marshal(nilSlice)\n    emptyJSON, _ := json.Marshal(emptySlice)\n\n    fmt.Printf("nilSlice JSON: %s\\n", nilJSON)\n    fmt.Printf("emptySlice JSON: %s\\n", emptyJSON)\n}
\n\n

The result will surprise you:

\n\n
nilSlice: [], is nil: true\nemptySlice: [], is nil: false\nnilSlice JSON: null\nemptySlice JSON: []
\n\n

Here it is, the main surprise! JSON serialization behaves differently. If your API expects [] in the response, but you pass a nil slice — the client will receive null. And this breaks contracts.

\n\n

Many REST clients (especially in JavaScript) strictly check: if an array is expected in the response, but null arrives — an error may occur or the frontend logic may break.

\n\n

Performance: Is There a Difference?

\n\n

The short answer: yes, but in 99% of cases it is insignificant.

\n\n

When you use make([]int, 0), Go immediately allocates memory for the slice structure (24 bytes on a 64-bit system) and links it to zerobase. When using var, allocation happens only on the first append.

\n\n

If you create a slice that will most likely remain empty, a nil slice saves one memory allocation. If you know for sure that you will be adding elements — make with a specified capacity will give better performance:

\n\n
// If you know the approximate size — specify the capacity\ns := make([]int, 0, 100)  // capacity 100, length 0\nfor i := 0; i < 100; i++ {\n    s = append(s, i)  // no reallocations!\n}
\n\n

But for most application tasks, the difference is

Blogs

Book Recommendations