Slices

Reviewed & published by Brayan K

Slices are the workhorse of Go. They're flexible, growable views onto an underlying array, and you'll reach for them constantly. You'll learn to create, append to, and re-slice them — and understand the one thing that surprises everyone: slices share their backing storage, so a copy can change the original.

Part of the free Go course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

1️⃣ Creating & Growing

A slice type has no length: []int. Make one from a literal, from make([]T, len), or start with a nil slice and grow it. The built-in append is how you add elements — and it returns a new slice, so you must always write s = append(s, x), never just append(s, x).

package main

import "fmt"

func main() {
    // A slice is a flexible, growable view onto a sequence.
    // No length in the type: []int, not [5]int.

    var s []int          // nil slice — length 0, ready to grow
    fmt.Println(s, len(s), s == nil) // [] 0 true

    // append RETURNS a new slice — always reassign the result.
    s = append(s, 10)
    s = append(s, 20, 30)        // append several at once
    fmt.Println(s)               // [10 20 30]

    // Slice literal — like an array but with no length.
    names := []string{"Ann", "Ben", "Cara"}
    fmt.Println(names, "len:", len(names)) // [Ann Ben Cara] len: 3

    // make builds a slice with a length (and optional capacity).
    grid := make([]int, 3)       // [0 0 0]
    grid[1] = 7
    fmt.Println(grid)            // [0 7 0]
}

2️⃣ Slicing, len & cap

The slice expression s[low:high] gives you elements from low up to (but not including) high. Omit either bound to mean "start" or "end". len is how many elements you have; cap is how many the backing array can hold before append must allocate a bigger one. Watch the last line carefully — appending within capacity writes through to the shared backing array.

package main

import "fmt"

func main() {
    nums := []int{0, 1, 2, 3, 4, 5}

    // The slice expression s[low:high] takes elements low..high-1.
    fmt.Println(nums[1:4])  // [1 2 3]  (index 1,2,3)
    fmt.Println(nums[:3])   // [0 1 2]  (from start)
    fmt.Println(nums[3:])   // [3 4 5]  (to end)

    // len = how many elements; cap = room before a regrow is needed.
    part := nums[1:3]
    fmt.Println("len:", len(part), "cap:", cap(part)) // len: 2 cap: 5

    // append within capacity reuses the same backing array.
    fmt.Println(part)            // [1 2]
    part = append(part, 99)
    fmt.Println(part)            // [1 2 99]
    fmt.Println("nums now:", nums) // nums now: [0 1 2 99 4 5]  (!)
}

3️⃣ References & copy()

Unlike arrays, a slice is a small header (pointer, length, capacity) that points at a backing array. Copying the header — by assignment or by passing to a function — shares that backing array, so mutations are visible everywhere. When you need a genuinely independent copy, use the built-in copy(dst, src) into a slice you make yourself.

package main

import "fmt"

func main() {
    // KEY IDEA: a slice is a REFERENCE to a backing array.
    // Copying the slice header shares the SAME elements.
    a := []int{1, 2, 3}
    b := a
    b[0] = 99
    fmt.Println("a:", a) // a: [99 2 3]  (changed — shared backing!)

    // To get an independent copy, use the built-in copy().
    src := []int{1, 2, 3}
    dst := make([]int, len(src))
    n := copy(dst, src)          // copies element-by-element
    dst[0] = 0
    fmt.Println("copied", n, "→ src:", src, "dst:", dst)
    // copied 3 → src: [1 2 3] dst: [0 2 3]

    // Passing a slice to a function lets it mutate the elements.
    zero(a)
    fmt.Println("after zero:", a) // after zero: [0 0 0]
}

func zero(s []int) {
    for i := range s {
        s[i] = 0
    }
}

Your turn — build a queue with append, then count it.

package main

import "fmt"

func main() {
    // 🎯 YOUR TURN — replace each ___ then run it.
    var queue []string

    // 1) Add three names. Remember append RETURNS the new slice.
    queue = ___(queue, "Ada")   // 👉 append
    queue = append(queue, "Bob", "Cy")

    // 2) How many are in the queue?
    fmt.Println(queue, "count:", ___(queue)) // 👉 len

    // ✅ Expected output:  [Ada Bob Cy] count: 3
}

Now grab the first three and last two with slice expressions.

package main

import "fmt"

func main() {
    // 🎯 YOUR TURN — finish the slice expressions then run it.
    letters := []string{"a", "b", "c", "d", "e"}

    first3 := letters[___]      // 👉 :3   -> [a b c]
    last2  := letters[___]      // 👉 3:   -> [d e]

    fmt.Println(first3, last2)

    // ✅ Expected output:  [a b c] [d e]
}

🧩 Reorder Challenge

These lines build a slice with append, but they're scrambled. Put them in order so the program prints [1 2 3] (assume a func main wraps them).

    s = append(s, 1)
    fmt.Println(s)
    var s []int
    s = append(s, 2, 3)

Why: the slice must be declared first (a nil slice is fine). Each append reassigns s with the returned slice — order matters because appending 1 then 2,3 gives [1 2 3]. The print comes last, after both appends.

Pro Tips

Common Errors (and the fix)

📋 Quick Reference

TaskSyntax
Literals := []int{1, 2, 3}
makemake([]int, 0, 10)
Appends = append(s, x)
Sub-slices[1:4]
Length / caplen(s), cap(s)
Copycopy(dst, src)

🧠 Quick Recall

Predict the output before you reveal the answer.

[2 3] — s[1:] starts at index 1 and runs to the end, dropping the first element.

a := []int{1,2}; b := a; b[0] = 9; fmt.Println(a[0])

9 — b := a copies the slice header, but both share the same backing array, so writing b[0] changes a[0] too.

var s []int; fmt.Println(len(s), s == nil)

0 true — an un-initialised slice is nil with length 0, yet it's still safe to append to.

Frequently Asked Questions

Q: What's the difference between len and cap?

len is how many elements the slice currently holds; cap is how many the backing array can hold before append must allocate a larger one and copy everything over.

Q: Why did changing my slice copy also change the original?

Both slices share the same backing array. Assignment copies the header, not the data. Use copy() into a fresh make'd slice for true independence.

Q: Do I need make if I'm going to append anyway?

Not strictly — appending to a nil slice works. But make([]T, 0, n) pre-allocates capacity, which avoids repeated reallocations when you know the rough size.

Mini-Challenge: Filter Evens

Loop the numbers, test each with n%2 == 0, and append the ones that pass into a fresh slice. Write it yourself and match the example output.

package main

import "fmt"

func main() {
    // 🎯 MINI-CHALLENGE: Filter evens
    // Given the numbers below, build a NEW slice containing only the
    // even values, then print it. Start with an empty []int and
    // append the ones that pass the n%2 == 0 test.
    //
    // ✅ Example output:  [2 4 6]

    nums := []int{1, 2, 3, 4, 5, 6}
    // your code here
}

🎉 Lesson Complete!

Practice quiz

What is the type of a slice of ints?

  • [5]int
  • slice[int]
  • []int
  • int[]

Answer: []int. A slice type has no length in it: []int, unlike an array [5]int.

What does append return, and how must you use it?

  • A new slice you must reassign: s = append(s, x)
  • It mutates in place; ignore the return
  • An error value
  • The appended element

Answer: A new slice you must reassign: s = append(s, x). append returns a (possibly new) slice, so always write s = append(s, x).

What is the value and nil-status of an uninitialised var s []int?

  • empty, not nil
  • length 1 and nil
  • it is a compile error
  • length 0 and s == nil is true

Answer: length 0 and s == nil is true. A zero-value slice is nil with length 0, yet it is still safe to append to.

What does s[1:4] evaluate to for s := []int{0,1,2,3,4,5}?

  • [1 2 3 4]
  • [1 2 3]
  • [0 1 2 3]
  • [2 3 4]

Answer: [1 2 3]. s[low:high] takes indices low..high-1, so 1,2,3 -> [1 2 3].

What is the difference between len and cap?

  • len is current elements; cap is room before append must reallocate
  • They are always equal
  • cap is elements; len is room
  • len is bytes; cap is elements

Answer: len is current elements; cap is room before append must reallocate. len is how many elements you have; cap is how many the backing array can hold.

After b := a (both []int) and b[0] = 99, what happens to a[0]?

  • a is unchanged
  • It panics
  • a[0] also becomes 99 because they share the backing array
  • a becomes nil

Answer: a[0] also becomes 99 because they share the backing array. Assignment copies the slice header; both share the same backing array.

How do you get a genuinely independent copy of a slice?

  • b := a
  • copy(dst, src) into a make'd slice
  • append(nil, a)
  • b := a[:]

Answer: copy(dst, src) into a make'd slice. copy(dst, src) into a freshly make'd slice gives independent storage.

What does copy return?

  • The destination slice
  • An error
  • Nothing
  • The number of elements copied

Answer: The number of elements copied. copy returns the number of elements copied: min(len(dst), len(src)).

Which is the #1 slice bug?

  • Using make
  • Writing append(s, x) without reassigning to s
  • Indexing s[0]
  • Calling len(s)

Answer: Writing append(s, x) without reassigning to s. Dropping the assignment loses the appended element — always s = append(s, x).

How do you pre-size a slice to avoid repeated regrows when you know the count?

  • make([]T, n, 0)
  • var s []T
  • make([]T, 0, n)
  • new([]T)

Answer: make([]T, 0, n). make([]T, 0, n) sets length 0 and capacity n so appends don't reallocate early.

Continue this course