Understanding Slice in Golang
Go’s slice is one of the most commonly used yet often misunderstood types. It looks like a simple dynamic array on the surface, but under the hood, slices have some subtle behaviors that every Go developer should understand to avoid bugs and performance issues.
What Is a Slice, Really?
A slice in Go is a lightweight data structure that provides a view into an underlying array. It is defined by three components:
1 | type slice struct { |
In other words, a slice does not own its data. It’s just a window over an array. This makes slicing operations fast — but also introduces pitfalls.
Slice Creation: Literal, make, and nil
There are multiple ways to create a slice:
1 | // Slice literal (backed by array of length 3) |
Understanding the difference between a nil slice and an empty but allocated slice is important when checking for initialization.
1 | var s []int // nil slice |
Slice Growth and Append
One of the most powerful features of slices is append:
1 | s := []int{1, 2} |
When the capacity is not enough, Go allocates a new backing array, copies the old elements, and appends the new ones.
But beware: this means if you share a slice between variables and append on one, the changes might or might not be visible to the others — depending on whether a new array was allocated.
1 | a := []int{1, 2, 3} |
To be safe, assume slices can share memory and mutate each other unless explicitly copied.
Copying Slice
If you want a true copy of a slice, use copy:
1 | original := []int{1, 2, 3} |
This ensures the two slices are backed by different arrays.
Slicing and Memory Leaks
Since slices point to the underlying array, if you take a small slice from a large array, the entire array remains in memory. This can cause unexpected memory leaks.
1 | big := make([]byte, 1<<20) // 1MB array |
Here, small still holds a reference to the entire 1MB array. To avoid this, explicitly copy the data:
1 | smallCopy := append([]byte(nil), small...) |
Reslicing and Capacity
You can reslice a slice as long as you stay within capacity:
1 | a := []int{1, 2, 3, 4, 5} |
This is useful for buffer reuse, but can also be dangerous if you’re not careful — reslicing too far will panic.
Pre-allocating for Performance
When you know the size of a slice in advance, pre-allocate it with make to avoid multiple reallocations:
1 | data := make([]int, 0, 1000) // zero-length slice with room for 1000 elements |
Using append on this slice is efficient because it avoids frequent copying during growth.
Gotchas and Tips
Slice assignment shares the same backing array:
1
2
3a := []int{1, 2}
b := a
b[0] = 9 // a[0] is also 9Modifying subslices affects the original:
1
2
3full := []int{1, 2, 3}
part := full[1:]
part[0] = 99 // full[1] == 99Be cautious with loop variables and slices:
1
2
3
4var res []*int
for _, v := range []int{1, 2, 3} {
res = append(res, &v) // All point to same `v`
}Instead, capture v in a new variable inside the loop.
Final Thoughts
Slices are powerful but subtly tricky. Understanding their backing array, growth behavior, and memory sharing is crucial for writing performant and bug-free Go code. Whether you’re building a high-throughput server or just manipulating data collections, a solid grasp of slices will take you far.
Further Reading: