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 array arr from index 1 to 3 (excluding index 4).
  • 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!