LearnAI ToolsCareerPractice BuildsPlayContact
Lesson 1719 min read

Slices

Learn Go's dynamic, flexible slice type: how append, len, and cap work, slicing syntax, and why slices dominate real-world Go code.

Introduction

Slices are the collection type you will use constantly in Go. Where an array has a fixed size baked into its type, a slice is a flexible, growable view over an underlying array — and it's what nearly every Go function, standard library API, and real-world program uses to represent a sequence of items.

What You Will Learn
  • What a slice is and how it relates to arrays.
  • Different ways to create a slice.
  • The difference between length and capacity.
  • How append grows a slice.
  • Slicing syntax for extracting sub-slices.
  • Why slices, not arrays, are Go's everyday collection type.

What Is a Slice?

A slice is a lightweight data structure consisting of three parts: a pointer to an underlying array, a length, and a capacity. Unlike an array, a slice's length can grow and shrink at runtime, and slices are reference types — copying a slice variable copies the pointer/length/capacity triple, not the underlying data.

package main
import "fmt"
func main() {
// Slice literal — looks like an array literal, minus the size.
fruits := []string{"apple", "banana", "cherry"}
fmt.Println(fruits)
fmt.Printf("Type: %T\n", fruits)
}
Output

Click Run to see what this code prints.

Creating Slices

There are several common ways to create a slice: a slice literal, slicing an existing array or slice, or the built-in make function, which is useful when you know roughly how many elements you'll need.

package main
import "fmt"
func main() {
// 1. Slice literal
a := []int{1, 2, 3}
// 2. From an array
arr := [5]int{10, 20, 30, 40, 50}
b := arr[1:4]
// 3. With make(type, length, capacity)
c := make([]int, 3, 5)
fmt.Println("a:", a)
fmt.Println("b:", b)
fmt.Println("c:", c, "len:", len(c), "cap:", cap(c))
}
Output

Click Run to see what this code prints.

Length and Capacity

Every slice has a length (len) — the number of elements it currently holds — and a capacity (cap) — the number of elements the underlying array can hold starting from the slice's first element, before Go needs to allocate a new array. Understanding the difference is key to understanding how append behaves.

package main
import "fmt"
func main() {
s := make([]int, 2, 4)
fmt.Println("len:", len(s), "cap:", cap(s)) // len: 2 cap: 4
s = append(s, 99)
fmt.Println("after append, len:", len(s), "cap:", cap(s)) // len: 3 cap: 4
}
Output

Click Run to see what this code prints.

Appending to a Slice

The built-in append function adds one or more elements to the end of a slice, returning a (possibly new) slice. If there's enough spare capacity, append reuses the existing underlying array. If not, Go allocates a new, larger array behind the scenes, copies the old elements over, and returns a slice pointing at the new array. This is why you must always assign the result of append back to a variable.

package main
import "fmt"
func main() {
var nums []int // nil slice, len 0, cap 0
for i := 1; i <= 5; i++ {
nums = append(nums, i*i)
fmt.Printf("len=%d cap=%d slice=%v\n", len(nums), cap(nums), nums)
}
// append can also spread another slice with "..."
more := []int{36, 49}
nums = append(nums, more...)
fmt.Println("final:", nums)
}
Output

Click Run to see what this code prints.

Growth Strategy

You don't need to memorize exact capacity growth numbers — they're an implementation detail that can change between Go versions. The key idea is that append amortizes the cost of growing, so appending is efficient on average even though a new array is occasionally allocated.

Slicing Syntax

You can extract a sub-slice using the syntax slice[low:high], which includes the element at low and excludes the element at high. Omitting low defaults to 0, and omitting high defaults to the slice's length.

package main
import "fmt"
func main() {
letters := []string{"a", "b", "c", "d", "e"}
fmt.Println(letters[1:3]) // [b c]
fmt.Println(letters[:2]) // [a b]
fmt.Println(letters[3:]) // [d e]
fmt.Println(letters[:]) // [a b c d e] (full copy of the header, same array)
}
Output

Click Run to see what this code prints.

Slices Share Underlying Arrays

Because a slice is just a view over an array, two slices produced from the same source can share memory. Modifying an element through one slice can be visible through the other, until an append forces a reallocation. This is a subtle but important behavior to understand.

package main
import "fmt"
func main() {
original := []int{1, 2, 3, 4, 5}
view := original[1:4] // shares the same underlying array
view[0] = 999 // mutates original too, since they share memory
fmt.Println("original:", original)
fmt.Println("view: ", view)
}
Output

Click Run to see what this code prints.

Common Mistakes

Avoid These Mistakes
  • Forgetting to reassign the result of append — nums = append(nums, x) is required, calling append(nums, x) alone has no effect.
  • Assuming a sub-slice is always an independent copy — it shares the underlying array unless a reallocation has occurred.
  • Confusing len and cap — len is what's currently in the slice, cap is the room available before reallocation.
  • Using a nil slice and being surprised it works — nil slices have len 0 and cap 0, and append handles them fine.
  • Slicing past the underlying array's bounds without checking, which causes a runtime panic.

Best Practices

  • Default to slices, not arrays, for any collection whose size can vary.
  • Use make([]T, 0, n) when you know roughly how many elements you'll append, to avoid repeated reallocations.
  • Use the copy() built-in when you need an independent slice that doesn't share memory with the original.
  • Pass slices to functions freely — they're cheap to copy since only the header (pointer/len/cap) is copied.
  • Be explicit about ownership when a function returns a sub-slice of an input slice, since callers may not expect shared memory.

Frequently Asked Questions

Not exactly — it's a small struct containing a pointer to the underlying array plus a length and capacity. Copying a slice copies that struct, so both copies point at the same array.

nil. A nil slice has length 0 and capacity 0, and it behaves safely with len(), range, and append — you don't need to initialize it before appending.

Use the built-in copy function: dst := make([]T, len(src)); copy(dst, src). This copies the elements themselves, not just the slice header.

If append triggered a reallocation (because capacity was exceeded), the new slice points at a brand-new array, so the original slice variable is now completely disconnected from it.

Key Takeaways

  • A slice is a flexible, growable view over an underlying array — pointer, length, and capacity.
  • append adds elements and returns a slice; always reassign its result.
  • len is the current element count; cap is the room before a new array must be allocated.
  • Slicing syntax s[low:high] extracts a sub-slice sharing the same underlying array.
  • Slices, not arrays, are the standard everyday collection type in idiomatic Go.

Summary

Slices give Go the flexibility that fixed-size arrays lack, while remaining efficient thanks to shared underlying arrays and amortized growth via append. Once you're comfortable with len, cap, and slicing syntax, you'll find yourself reaching for slices in nearly every Go program you write. Next, you'll learn about maps, Go's built-in key-value collection type.

Next Lesson →

Maps