Skip to content

Beginner · Collections and structs · topic 12 of 46 · 40 min

Slices in depth: length, capacity and sharing

Explanation

A slice is a small header with three parts: a pointer to an array, a length and a capacity. Slices made from the same array share data, so changing one can change another, and append may or may not create a new array. Knowing this explains most slice bugs. Since Go 1.21 the slices package gives ready-made Sort, Contains, Index and more.

What a slice really is

A slice value is just three numbers: a pointer to the first element it can see in an underlying array, a length (how many elements it shows) and a capacity (how many elements there are from its start to the end of that array). The data itself lives in the array.

Think of it like this: Think of a long bench in a school hall (the array). A slice is a teacher saying "my group is from seat 2 to seat 5". Two teachers can point at overlapping seats. If one student moves a bag on seat 3, both groups see it. Capacity is how many seats remain from the group's first seat to the end of the bench.
go
arr := [6]int{0, 1, 2, 3, 4, 5}
s := arr[1:4]
fmt.Println(s, len(s), cap(s)) // [1 2 3] 3 5
Length 3 (elements 1,2,3). Capacity 5, from index 1 to the end of the 6-element array.

Sharing: one change, two slices

go
a := []int{1, 2, 3, 4}
b := a[:2]       // [1 2], shares a's array, cap 4
b[0] = 100
fmt.Println(a)   // [100 2 3 4]

b = append(b, 99) // room left in the array: writes into a[2]!
fmt.Println(a)    // [100 2 99 4]
fmt.Println(b)    // [100 2 99]
Because b still had capacity, append wrote into the shared array and overwrote a[2]. This is the most famous slice bug.

When append makes a new array

If the slice has no spare capacity, append allocates a bigger array, copies the elements over, and returns a slice pointing to the new array. From then on, the old and new slices no longer share data. Because you cannot always predict which case happens, always use the result of append and do not rely on sharing after append.

go
x := make([]int, 3, 3)  // len 3, cap 3, full
y := append(x, 4)        // no room: new array
y[0] = 7
fmt.Println(x, y)        // [0 0 0] [7 0 0 4]
y lives in a new array, so changing y[0] did not touch x.

Protecting data: copy and the full slice expression

go
src := []int{1, 2, 3}
dst := make([]int, len(src))
n := copy(dst, src)       // copies min(len(dst), len(src)) elements
dst[0] = 50
fmt.Println(src, dst, n)  // [1 2 3] [50 2 3] 3

safe := src[0:2:2]        // low:high:max, cap is max-low = 2
safe = append(safe, 9)    // must allocate, src untouched
fmt.Println(src, safe)    // [1 2 3] [1 2 9]
copy needs a destination with enough LENGTH (not just capacity). s[low:high:max] limits capacity so append cannot overwrite the original.
Tip: Since Go 1.21, slices.Clone(s) makes an independent copy in one call.

nil slice versus empty slice

go
var a []int          // nil slice
b := []int{}         // empty, not nil
fmt.Println(a == nil, b == nil)   // true false
fmt.Println(len(a), len(b))       // 0 0
a = append(a, 1)                  // append works on nil slices too
Both have length 0 and work with len, range and append. Prefer checking len(s) == 0 rather than s == nil.

One place they differ: encoding/json turns a nil slice into null and an empty slice into []. An API that must return [] for "no orders" should start with an empty slice.

The slices package (Go 1.21+)

go
import "slices"

prices := []int{450, 120, 999, 60}
slices.Sort(prices)                         // [60 120 450 999]
fmt.Println(slices.Contains(prices, 120))   // true
fmt.Println(slices.Index(prices, 999))      // 3
fmt.Println(slices.Index(prices, 5))        // -1 (not found)
fmt.Println(slices.Max(prices))             // 999
slices.Reverse(prices)                      // [999 450 120 60]
fmt.Println(slices.Equal(prices, []int{999, 450, 120, 60})) // true
These are generic: they work for slices of any suitable type. slices.Max panics on an empty slice.

Go 1.21 also added built-in min and max functions for values: min(3, 7, 1) is 1. Before that, people wrote small helpers or used the older sort package (sort.Ints, sort.Strings), which you will still see in older code.

Removing an element

go
s := []string{"a", "b", "c", "d"}
i := 1
s = append(s[:i], s[i+1:]...)   // classic way: [a c d]
// or, Go 1.21+:
// s = slices.Delete(s, i, i+1)
Both reuse the same array, so other slices that share it will see the elements shift.
Careful: Common bug: a function receives a slice, appends to it, and the caller does not see the new items. The function got a copy of the header; append may have changed the copy's length or array. Return the new slice: func add(s []int, v int) []int { return append(s, v) }.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Slices in depth: length, capacity and sharing · 1/7

Slices in depth

Slices are simple to use, but to avoid bugs you need to know what is inside one.

Prefer a video by a teacher? Find free videos on “golang slice length capacity append internals explained” on YouTube ↗

In real life: Zerodha-style top gainers

A stock app has today's price changes for many companies. To show the top 3 gainers, it clones the slice (so the original order used elsewhere is untouched), sorts the clone, and takes the last three. Without the clone, sorting would silently reorder the shared data that another part of the screen is still using.

go
package main

import (
	"fmt"
	"slices"
)

func main() {
	changes := []float64{1.2, -0.8, 3.5, 0.4, 2.1, -1.9}
	sorted := slices.Clone(changes)
	slices.Sort(sorted)
	top3 := sorted[len(sorted)-3:]
	slices.Reverse(top3)
	fmt.Println("Top gainers %:", top3)  // [3.5 2.1 1.2]
	fmt.Println("Original:", changes)    // unchanged order
}
slices.Clone protects the original; slices.Reverse puts the biggest first.

Practice (0/4 done)

Task 1 · Question

What does this print? s := make([]int, 2, 10) fmt.Println(len(s), cap(s))

Task 2 · Question

What does this print? a := []int{1, 2, 3, 4} b := a[1:3] b[0] = 9 fmt.Println(a)

Task 3 · Question

What does this print? a := []int{1, 2, 3} b := a[:1] b = append(b, 7) fmt.Println(a)

Task 4 · Practice

Write func removeAt(s []string, i int) []string that removes the element at index i WITHOUT changing the caller's original slice. Show in main that the original still has all its elements.

Quick quiz

Question 1

1.Which three things make up a slice header?

Question 2

2.var s []int; s = append(s, 5). What happens?

Question 3

3.How many elements does copy(dst, src) copy?

Question 4

4.What does slices.Index(s, x) return when x is not in s?

Question 5

5.Why use s[0:2:2] before appending?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

Which three things make up a slice header?

Answer: Pointer to array, length, capacity

A slice is a pointer, a length and a capacity.

var s []int; s = append(s, 5). What happens?

Answer: It works; s is [5]

append works on a nil slice and allocates an array.

How many elements does copy(dst, src) copy?

Answer: The smaller of len(dst) and len(src)

copy never grows dst; it copies min(len(dst), len(src)) elements.

What does slices.Index(s, x) return when x is not in s?

Answer: -1

Like many Go search functions, it returns -1 for "not found".

Why use s[0:2:2] before appending?

Answer: To limit capacity so append must allocate and cannot overwrite the original array

The third index sets capacity; with no spare room, append copies to a new array.

Can you compare two slices with ==?Related

Answer: No, only a slice with nil

Slices are not comparable; use slices.Equal to compare contents.

From Arrays and slices

How does Go provide polymorphism without inheritance?Related

Answer: Interfaces satisfied implicitly

Any type with the right methods can be used through an interface.

From Go language interview questions

Can you do p++ on a pointer in normal Go code?Related

Answer: No, Go has no pointer arithmetic

Pointer arithmetic is not allowed (outside the unsafe package).

From Pointers without fear

What does a slice contain?Related

Answer: A pointer and a length

A slice is a fat pointer: where it starts and how many items.

From Slices and strings

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 3 more topics in “Collections and structs” before its exam: Strings, bytes and runes, Maps: key-value look-ups, Structs and methods.
  2. Module 3 exam: Collections and structs · 15 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Pointers, interfaces and errors. 46 topics left in the course before the final exam.

← Previous: Arrays and slices