Empty Slice in Go: How to Avoid Mistakes from the Start
\n\nEvery 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\nBecause 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\nWe will cover:
\n- \n
- How
var s []intdiffers froms := 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
Two Siblings: nil slice vs empty slice
\n\nIn 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\nMethod 1: var declaration (nil slice)
\n\nvar s []int // s == nil, len(s) == 0, cap(s) == 0\n\nThis 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.
Method 2: make initialization (empty slice)
\n\ns := make([]int, 0) // s != nil, len(s) == 0, cap(s) == 0\n\nThis is an empty slice. It already points to a zero-length array (a special zerobase in the Go runtime).
\n\nAt first glance, the difference is unnoticeable. But let's dig deeper.
\n\nConsole Output and JSON — The First Pitfall
\n\nTry running this code:
\n\npackage 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\nThe result will surprise you:
\n\nnilSlice: [], is nil: true\nemptySlice: [], is nil: false\nnilSlice JSON: null\nemptySlice JSON: []\n\nHere 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.
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.
Performance: Is There a Difference?
\n\nThe short answer: yes, but in 99% of cases it is insignificant.
\n\nWhen 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.
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:
// 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\nBut for most application tasks, the difference is