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
- Create slices with literals, make, and append
- Grow slices and understand append returns a new slice
- Re-slice with s[low:high], s[:n], s[n:]
- Tell len from cap and why cap matters
- Know slices are references that share backing storage
- Make a true copy with the built-in copy()
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)- s = append(s, 1)
- s = append(s, 2, 3)
- fmt.Println(s)
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
- 💡 Always reassign: s = append(s, x). Dropping the assignment is the #1 slice bug.
- 💡 Pre-size with make([]T, 0, n) when you know roughly how many elements are coming — it avoids repeated regrows.
- 💡 Use copy() for independence. If you need a slice that won't move with the original, copy into a fresh one.
- 💡 Beware sub-slices keeping big arrays alive. A tiny sub-slice of a huge slice still references the whole backing array; copy it out to free the rest.
Common Errors (and the fix)
- "append result discarded"-style bugs — you wrote append(s, x) without s =. The new element vanishes. Reassign the result.
- Mutating a copy changes the original — that's expected: slices share backing storage. Use copy() for an independent slice.
- "slice bounds out of range [:6] with capacity 5" — you re-sliced past the capacity. Check cap(s) first, or use append.
- "index out of range" on a nil or empty slice — you indexed before appending. A nil slice has length 0; append before you index.
📋 Quick Reference
| Task | Syntax |
|---|---|
| Literal | s := []int{1, 2, 3} |
| make | make([]int, 0, 10) |
| Append | s = append(s, x) |
| Sub-slice | s[1:4] |
| Length / cap | len(s), cap(s) |
| Copy | copy(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!
- ✅ Slices ([]T) are flexible, growable views — no length in the type
- ✅ append returns a new slice; always write s = append(s, x)
- ✅ Re-slice with s[low:high]; len is size, cap is room to grow
- ✅ Slices are references sharing a backing array — copies can change the original
- ✅ Use copy(dst, src) for a genuinely independent slice
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.