Go Slices

Su Suriya Ravichandran Updated 14 Sep 2026
7 min read ·Lesson 10 of 12

Slices are one of the most powerful and commonly used features in Go. They provide a flexible, resizable view over a sequence of elements, which makes them far more practical than fixed-size arrays for everyday programming.

What Is a Slice?

A slice is a dynamically-sized, flexible view into an underlying array. Like arrays, a slice stores elements of a single type — but unlike an array, a slice's length can grow or shrink as your program runs.

Under the hood, a slice is a small structure with three parts: a pointer to an underlying array, a length, and a capacity. Understanding this internal structure explains most of the "surprising" slice behavior you'll encounter later (such as why appending sometimes affects a related slice and sometimes doesn't).

Key benefits of slices:

  • Dynamic size — can grow and shrink.
  • A rich set of built-in functions (append, copy, len, cap) for working with them.
  • Used far more often than arrays in real-world Go code — arrays are mostly an implementation detail slices are built on.

Three Ways to Create a Slice

1. Slice Literals

sliceName := []datatype{values}
package main

import "fmt"

func main() {
    // Empty slice
    numbers := []int{}
    fmt.Println("numbers:", numbers)
    fmt.Println("Length:", len(numbers))
    fmt.Println("Capacity:", cap(numbers))

    // Initialized slice
    fruits := []string{"Apple", "Banana", "Cherry"}
    fmt.Println("\nfruits:", fruits)
    fmt.Println("Length:", len(fruits))
    fmt.Println("Capacity:", cap(fruits))
}

Expected output:

numbers: []
Length: 0
Capacity: 0

fruits: [Apple Banana Cherry]
Length: 3
Capacity: 3

Understanding len() and cap():

  • len(slice) — the number of elements currently in the slice.
  • cap(slice) — the number of elements the underlying array can hold before Go needs to allocate a new, larger array.

2. Slicing an Array

You can derive a slice from an array (or from another slice) using an index range:

slice := array[start:end]

The result includes index start up to, but not including, index end.

package main

import "fmt"

func main() {
    grades := [6]int{85, 90, 78, 92, 88, 76}

    // From index 1 up to (not including) index 4
    selected := grades[1:4]

    fmt.Println("Selected grades:", selected)
    fmt.Println("Length:", len(selected))
    fmt.Println("Capacity:", cap(selected))
}

Expected output:

Selected grades: [90 78 92]
Length: 3
Capacity: 5

The slice's capacity is measured from its start index to the end of the underlying array, not just to the slice's own end — here, the array has 6 elements, the slice starts at index 1, so capacity is 6 - 1 = 5, even though the slice's length is only 3.

3. Using make()

make() creates a slice with a predefined length and (optionally) capacity, backed by a freshly allocated array:

slice := make([]type, length, capacity)

If capacity is omitted, it defaults to equal length.

package main

import "fmt"

func main() {
    // Length 3, capacity 6 — room to grow before reallocating
    buffer := make([]int, 3, 6)
    fmt.Println("buffer:", buffer)
    fmt.Println("Length:", len(buffer))
    fmt.Println("Capacity:", cap(buffer))

    // Length 4, capacity defaults to 4
    queue := make([]int, 4)
    fmt.Println("\nqueue:", queue)
    fmt.Println("Length:", len(queue))
    fmt.Println("Capacity:", cap(queue))
}

Expected output:

buffer: [0 0 0]
Length: 3
Capacity: 6

queue: [0 0 0 0]
Length: 4
Capacity: 4

make() is the standard way to create a slice when you know roughly how many elements you'll need in advance, since pre-allocating capacity avoids repeated reallocation as you append elements.

When to Use Slices

Reach for a slice whenever:

  • The number of elements isn't known ahead of time.
  • You need to grow or shrink a collection while the program runs.
  • You want to pass a collection to a function cheaply — a slice header is small and inexpensive to copy, unlike a whole array.

Accessing and Modifying Slices

Accessing Elements

Indexing works exactly like arrays — starting from 0:

package main

import "fmt"

func main() {
    scores := []int{85, 90, 78, 92}

    fmt.Println("First score:", scores[0])
    fmt.Println("Last score:", scores[3])
}

Modifying Elements

package main

import "fmt"

func main() {
    temperatures := []int{28, 30, 32}

    temperatures[1] = 35 // update the second value

    fmt.Println("Updated temperatures:", temperatures)
}

Expected output:

Updated temperatures: [28 35 32]

Appending Elements

append() adds one or more elements to the end of a slice and returns the resulting slice — you must always assign the result back, since append may or may not return the same underlying array:

slice = append(slice, elements...)
package main

import "fmt"

func main() {
    ids := []int{101, 102, 103}

    fmt.Println("Before append:", ids)
    fmt.Println("Length:", len(ids), "Capacity:", cap(ids))

    ids = append(ids, 104, 105)

    fmt.Println("\nAfter append:", ids)
    fmt.Println("Length:", len(ids), "Capacity:", cap(ids))
}

Expected output:

Before append: [101 102 103]
Length: 3 Capacity: 3

After append: [101 102 103 104 105]
Length: 5 Capacity: 6

When an append exceeds the current capacity, Go allocates a brand-new, larger underlying array (commonly doubling the previous capacity for smaller slices), copies the existing elements over, and returns a slice pointing at the new array. This is why you must always capture append's return value — the original slice variable is not updated in place.

Common mistake: forgetting that ids = append(ids, ...) is required, and instead writing just append(ids, 104, 105) without reassigning. The call still runs, but the new elements are discarded because nothing captured the returned slice.

Merging Two Slices

Use append() with the ... spread operator to append every element of one slice onto another:

package main

import "fmt"

func main() {
    even := []int{2, 4, 6}
    odd := []int{1, 3, 5}

    combined := append(even, odd...)

    fmt.Println("Combined slice:", combined)
    fmt.Println("Length:", len(combined))
    fmt.Println("Capacity:", cap(combined))
}

Expected output:

Combined slice: [2 4 6 1 3 5]
Length: 6
Capacity: 6

Watch out: if even had spare capacity, append(even, odd...) could overwrite elements inside even's existing underlying array rather than allocating a new one — which could silently corrupt data if another slice was still sharing that array. When in doubt about aliasing, use make() plus copy() to build an independent result, as shown below.

Changing a Slice's Length

Unlike arrays, slices can shrink and grow through re-slicing and append:

package main

import "fmt"

func main() {
    data := [7]int{10, 20, 30, 40, 50, 60, 70}

    slice := data[2:6]
    fmt.Println("Initial:", slice, "Len:", len(slice), "Cap:", cap(slice))

    slice = data[2:4]
    fmt.Println("Resliced:", slice, "Len:", len(slice), "Cap:", cap(slice))

    slice = append(slice, 80, 90, 100)
    fmt.Println("Expanded:", slice, "Len:", len(slice), "Cap:", cap(slice))
}

Expected output:

Initial: [30 40 50 60] Len: 4 Cap: 5
Resliced: [30 40] Len: 2 Cap: 5
Expanded: [30 40 80 90 100] Len: 5 Cap: 10

Notice the "Expanded" append exceeded the original capacity of 5, so Go allocated a fresh backing array — slice is now completely independent of data.

Memory Efficiency with copy()

A slice keeps its entire underlying array alive in memory for as long as any slice still references it — even a small slice into one index of a huge array prevents the rest of that array from being garbage collected. To avoid holding onto memory you no longer need, use copy() to build a smaller, independent slice.

copy(destination, source)
  • Copies elements from source into destination.
  • Copies as many elements as fit in the shorter of the two slices.
  • Returns the number of elements actually copied.
package main

import "fmt"

func main() {
    rawData := []int{5, 10, 15, 20, 25, 30, 35, 40}

    fmt.Println("Original:", rawData)
    fmt.Println("Length:", len(rawData), "Capacity:", cap(rawData))

    subset := rawData[:3]

    // A new, independent slice sized exactly to the data we need
    optimized := make([]int, len(subset))
    copy(optimized, subset)

    fmt.Println("\nOptimized copy:", optimized)
    fmt.Println("Length:", len(optimized), "Capacity:", cap(optimized))
}

Expected output:

Original: [5 10 15 20 25 30 35 40]
Length: 8 Capacity: 8

Optimized copy: [5 10 15]
Length: 3 Capacity: 3

Why this matters: once optimized exists, the original rawData array can be garbage collected as soon as nothing else references it — important in long-running programs that process large datasets in bursts.

Best Practices

  • Prefer slices over arrays unless you have a specific reason to fix the size (for example, modeling something inherently fixed-length, like an RGB color's three components).
  • Always reassign the result of append() — never assume it mutates in place.
  • Use make() with an estimated capacity up front when you know roughly how large a slice will grow, to avoid repeated reallocation.
  • Use copy() when you need to detach a small slice from a large underlying array to free memory.

0 Comments

Reviewed before they appear

No comments yet.

Go Lang
Ask about this post
AI Ask about this post

Ask questions about Go Slices and get answers drawn from it.

Signed-in readers only.