When writing Go programs, you’ll find that arrays are often too rigid to be practical. That’s where slices come into play. Slices are Go’s built-in abstraction over arrays that offer flexibility, power, and performance.
In this blog, we’ll explore:
- What slices are
- How they work internally
- How to create and use them
- Common pitfalls
- Best practices with examples
📌 What is a Slice?
A slice is a dynamically-sized, flexible view into the elements of an array. It doesn't store data itself — it points to an underlying array.
Slice Structure (Internally)
A slice in Go is a small struct with 3 fields:
type slice struct {
ptr *T // pointer to the underlying array
len int // number of elements in the slice
cap int // total capacity (from ptr to end of array)
}
✅ Creating Slices
1. From an Array
arr := [5]int{10, 20, 30, 40, 50}
slice := arr[1:4] // [20 30 40]
Here:
-
slice
references the arrayarr
from index1
to3
(excluding index4
). -
len(slice)
= 3 -
cap(slice)
= 4 (from index 1 to end of the array)
2. Using Slice Literal
numbers := []int{1, 2, 3}
This creates both a slice and its underlying array.
3. Using make()
s := make([]int, 3) // [0 0 0]
s := make([]int, 3, 5) // len=3, cap=5
🔁 Slicing a Slice
data := []int{100, 200, 300, 400}
sub := data[1:3] // [200 300]
This sub-slice shares memory with the original slice. Changing sub
affects data
:
sub[0] = 999
fmt.Println(data) // [100 999 300 400]
➕ Appending Elements
nums := []int{1, 2}
nums = append(nums, 3, 4) // [1 2 3 4]
If the slice’s capacity is full, Go automatically allocates a new underlying array.
⚠️ Shared Memory Behavior
arr := []int{1, 2, 3, 4}
s1 := arr[0:2] // [1 2]
s2 := arr[1:3] // [2 3]
s1[1] = 999
fmt.Println(arr) // [1 999 3 4]
fmt.Println(s2) // [999 3]
Tip:
To avoid this, use copy()
:
original := []int{1, 2, 3}
clone := make([]int, len(original))
copy(clone, original)
📈 Capacity Growth with append()
Go typically doubles the capacity when appending exceeds the current capacity.
s := make([]int, 0, 2)
s = append(s, 1, 2, 3)
fmt.Println(s) // [1 2 3]
If the capacity is exceeded, a new array is allocated behind the scenes.
📦 Built-in Functions
Function | Description |
---|---|
len(slice) |
Returns number of elements |
cap(slice) |
Returns total capacity |
append(slice, elems...) |
Appends elements |
copy(dst, src) |
Copies elements |
🧪 Complete Example
func main() {
original := []int{10, 20, 30, 40, 50}
slice1 := original[1:4]
fmt.Println("Slice1:", slice1) // [20 30 40]
slice2 := append(slice1, 99)
fmt.Println("Original:", original)
fmt.Println("Slice2:", slice2)
slice1[0] = 777
fmt.Println("Modified Slice1:", slice1)
fmt.Println("Modified Original:", original)
}
Output (May vary based on capacity reuse):
Slice1: [20 30 40]
Original: [10 20 30 40 99]
Slice2: [20 30 40 99]
Modified Slice1: [777 30 40]
Modified Original: [10 777 30 40 99]
❗ Common Mistakes
🔹 Assuming slices are independent:
a := []int{1, 2, 3}
b := a
b[0] = 999
fmt.Println(a) // [999 2 3]
Use copy()
to truly clone a slice.
🔹 Forgetting that append()
might create a new array
Always check whether your slice is still pointing to the original memory if you rely on shared behavior.
✅ Best Practices
- Use slices for most collections — they’re more idiomatic and dynamic than arrays.
- Use
copy()
when independence is needed. - Avoid unnecessary slicing inside performance-critical loops.
- Use
make([]T, len, cap)
if you know the size ahead of time.
🧠 Summary
Feature | Array | Slice |
---|---|---|
Size | Fixed | Dynamic |
Memory Sharing | Yes | Yes |
Append Support | No | Yes |
Preferred Use | Rare | Always |
🔚 Final Thoughts
Slices are a cornerstone of Go’s design, combining performance with flexibility. Understanding how slices share memory with arrays, grow, and interact with built-in functions is essential for writing idiomatic and safe Go code.
Once you master slices, you’ll write cleaner, more efficient programs — and avoid some tricky bugs!