Question
In Go, what is the preferred way to declare an empty slice whose size is not fixed?
For example, is it better to write:
mySlice1 := make([]int, 0)
or:
mySlice2 := []int{}
Which approach is correct, and when should each one be used?
Short Answer
By the end of this page, you will understand how empty slices work in Go, the difference between make([]T, 0) and []T{}, when they behave the same, and when make becomes useful because of capacity planning.
Concept
In Go, a slice is a flexible view over an underlying array. Slices are commonly used when you want a collection that can grow or shrink.
Both of these create an empty slice:
make([]int, 0)
[]int{}
In practice, both are valid and both produce a slice with:
- length
0 - capacity
0 - non-
nilslice value
That means for many cases, they are effectively equivalent.
The key idea is this:
[]int{}is a slice literalmake([]int, 0)creates a slice using Go's built-inmakefunction
Why does this matter?
Because make becomes especially useful when you want to specify capacity ahead of time:
numbers := make([]int, 0, 100)
This creates an empty slice with room for 100 elements before Go needs to allocate a larger backing array.
So the usual guidance is:
- Use
[]T{}when you simply want an empty slice. - Use
make([]T, 0, capacity)when you know roughly how many items you will append.
This matters in real programs because slices are used everywhere: collecting results, building JSON data, processing records, accumulating errors, and more. Choosing the right initialization style can make code clearer and sometimes more efficient.
Mental Model
Think of a slice like a shopping cart.
- The length is how many items are currently in the cart.
- The capacity is how much space the cart currently has before you need a bigger one.
Now compare the two forms:
[]int{}gives you an empty cart.make([]int, 0)also gives you an empty cart.make([]int, 0, 10)gives you an empty cart that already has room for 10 items.
So if you do not care about reserved space, both empty-cart versions are fine. If you want to prepare for future appends, make is the tool designed for that.
Syntax and Examples
Core syntax
Empty slice with a literal
numbers := []int{}
Empty slice with make
numbers := make([]int, 0)
Empty slice with preallocated capacity
numbers := make([]int, 0, 5)
Example: both forms work the same for appending
package main
import "fmt"
func main() {
one := []int{}
two := make([]int, 0)
one = append(one, 10)
two = append(two, 20)
fmt.Println(one, len(one), cap(one))
fmt.Println(two, len(two), cap(two))
}
Possible output:
[10] 1 1
[20] 1 1
Both slices start empty and both can grow with append.
Example: using capacity when you expect growth
package main
import "fmt"
func main() {
values := make([]int, 0, 3)
fmt.Println(len(values), cap(values))
values = append(values, 1)
values = append(values, 2)
values = append(values, 3)
fmt.Println(values)
fmt.Println(len(values), cap(values))
}
This is useful when you know you will append several values and want to avoid unnecessary reallocations.
Step by Step Execution
Consider this example:
package main
import "fmt"
func main() {
scores := make([]int, 0, 2)
fmt.Println(scores, len(scores), cap(scores))
scores = append(scores, 90)
fmt.Println(scores, len(scores), cap(scores))
scores = append(scores, 95)
fmt.Println(scores, len(scores), cap(scores))
}
Step by step:
-
scores := make([]int, 0, 2)- Creates an empty slice of
int - Length is
0because it contains no elements yet - Capacity is
2because space for 2 elements is reserved
- Creates an empty slice of
-
First
Println- Prints
[] 0 2 - The slice is empty, but it is ready to hold two values without growing
- Prints
-
scores = append(scores, 90)- Adds
90to the slice - Length becomes
1 - Capacity stays
2
- Adds
-
Second
Println- Prints something like
[90] 1 2
- Prints something like
-
scores = append(scores, 95)- Adds
95 - Length becomes
2 - Capacity is still
2
- Adds
-
Third
Println- Prints something like
[90 95] 2 2
- Prints something like
The important lesson is that an empty slice is still fully usable. It simply starts with no elements.
Real World Use Cases
Empty slices are common in real Go programs.
Collecting database results
You may query rows and append each result:
users := []User{}
or, if you expect many rows:
users := make([]User, 0, 100)
Building API responses
When preparing JSON arrays for an HTTP response, you often start with an empty slice and append items as you process data.
Filtering data
If you loop through values and keep only matches, an empty slice is the natural starting point:
var evens []int
for _, n := range nums {
if n%2 == 0 {
evens = append(evens, n)
}
}
Collecting validation errors
Applications often build a list of problems before returning them:
errors := []string{}
Preallocating for performance
When parsing a file with a known number of records, make([]T, 0, n) can reduce repeated allocations.
Real Codebase Usage
In real codebases, developers choose the initialization style based on intent.
Common patterns
Use var s []T for a nil slice when nil is acceptable
var items []string
This is common when:
- you want the zero value
nilhas a meaningful distinction- you will append later
Use []T{} for a clearly non-nil empty slice
items := []string{}
This is useful when you want to signal: "this is intentionally an empty collection."
Use make([]T, 0, n) when capacity matters
items := make([]string, 0, expectedCount)
This is common in:
- parsers
- data import code
- batch processing
- performance-sensitive loops
Practical pattern: append in loops
results := make([]Result, 0, len(input))
for _, item := range input {
results = append(results, process(item))
}
Practical pattern: JSON responses
Some teams prefer returning empty arrays instead of null in JSON. In those cases, a non-nil empty slice such as []T{} may be chosen deliberately.
Practical pattern: guard clause with empty result
if len(source) == 0 {
return []int{}
}
This can be used when an API contract prefers an empty slice over nil.
Common Mistakes
1. Confusing empty slices with nil slices
These are different:
var a []int // nil slice
b := []int{} // empty, non-nil slice
c := make([]int, 0) // empty, non-nil slice
They often behave similarly with len, cap, and append, but they are not identical.
fmt.Println(a == nil) // true
fmt.Println(b == nil) // false
fmt.Println(c == nil) // false
Avoid this mistake by deciding whether nil vs empty matters in your program.
2. Using make([]int, 10) when you meant "empty with room for 10"
Broken intent:
numbers := make([]int, 10)
This does not create an empty slice with capacity 10. It creates a slice with:
- length
10 - capacity
10
That means it already contains 10 zero values.
Correct version:
numbers := make([]int, 0, 10)
3. Thinking one form is universally more correct
Both are correct:
a := []int{}
b := make([]int, 0)
Choose based on readability and whether you need to control capacity.
4. Forgetting to assign the result of append
Broken code:
numbers := []int{}
append(numbers, 1)
Correct code:
numbers := []int{}
numbers = append(numbers, 1)
append returns the updated slice.
5. Overusing make([]T, 0) when a literal is simpler
This is valid:
names := make([]string, 0)
But if capacity is not important, this is often shorter and clearer:
names := []string{}
Comparisons
| Form | Result | Nil? | Best use |
|---|---|---|---|
var s []int | Empty slice value with zero value semantics | Yes | When nil is fine or preferred |
[]int{} | Empty slice literal | No | When you want a simple non-nil empty slice |
make([]int, 0) | Empty slice created with make | No | Valid, but most useful when extended with capacity |
make([]int, 0, 10) | Empty slice with capacity 10 | No | When you expect future appends |
make([]int, 10) | Slice with 10 zero-value elements | No | When you need 10 existing elements immediately |
[]T{} vs make([]T, 0)
- Both are correct.
- Both create empty, non-nil slices.
[]T{}is often more concise.make([]T, 0, cap)is better when planning capacity.
Empty slice vs nil slice
| Behavior | Nil slice | Empty non-nil slice |
|---|---|---|
len(s) | 0 | 0 |
cap(s) | 0 | 0 or more |
append(s, x) | Works | Works |
s == nil | true | false |
| JSON output in some contexts | May become null | May become [] |
Cheat Sheet
// Nil slice
var s []int
// Empty, non-nil slice
s := []int{}
// Empty, non-nil slice using make
s := make([]int, 0)
// Empty slice with reserved capacity
s := make([]int, 0, 10)
// Slice with length 10 (already contains 10 zero values)
s := make([]int, 10)
Quick rules
- Use
[]T{}for a simple empty slice. - Use
make([]T, 0, n)when you know expected capacity. - Use
var s []Twhen anilslice is acceptable. - Do not confuse
make([]T, n)withmake([]T, 0, n). - Always assign the result of
append.
Important checks
len(s) // number of elements
cap(s) // reserved space
s == nil // only true for nil slices
Rule of thumb
- No capacity needs?
[]T{} - Need capacity?
make([]T, 0, n) - Want zero-value nil slice?
var s []T
FAQ
Is make([]int, 0) better than []int{} in Go?
Not generally. Both are correct for creating an empty non-nil slice. Use make when you also want to set capacity.
What is the difference between an empty slice and a nil slice in Go?
A nil slice has no underlying slice value and compares equal to nil. An empty slice has length 0 but is not nil.
Should I use var s []int or []int{}?
Use var s []int if a nil slice is acceptable. Use []int{} if you specifically want a non-nil empty slice.
Why would I use make([]T, 0, n)?
It lets you reserve capacity for future appends, which can reduce allocations and improve performance.
Does append work on a nil slice?
Yes. Appending to a nil slice works in Go.
What does make([]int, 10) do?
It creates a slice with length 10 and capacity 10, filled with zero values. It is not empty.
Which style is more idiomatic in Go?
For a simple empty slice, []T{} is often clearer. For preallocated capacity, make([]T, 0, n) is idiomatic.
Mini Project
Description
Build a small Go program that collects even numbers from a list. This demonstrates when to start with an empty slice and when to preallocate capacity for future append calls.
Goal
Create a function that filters a slice of integers and returns only the even numbers using an empty slice correctly.
Requirements
- Write a function named
filterEvensthat accepts[]intand returns[]int. - Start with an empty result slice.
- Append only even numbers to the result.
- Print the final slice, its length, and its capacity.
- Add a version that preallocates capacity using
make.
Keep learning
Related questions
Are Go Maps Passed by Value or Reference?
Learn Go map parameter and return semantics: maps are passed by value, while copies share underlying data. Know when map pointers are useful.
Authentication for Go REST APIs: Sessions, Tokens, and Middleware
Learn practical authentication patterns for Go REST APIs, including password hashing, server sessions, bearer tokens, middleware, and security rules.
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.