← Fundamentals
Guided lesson 7 questions · Go only

Go slice headers, append, and capacity

When two slices share an array, and when append gives one of them a new array.

A slice is a pointer, a length, and a capacity over an array. Slicing and copying a slice share that array. append writes into the array when the capacity allows and moves to a new array when it does not, so whether two slices share elements depends on capacity.

Written for Go.

Before you begin

Why this matters

Most slice bugs in Go come from one question: do these two slices use the same elements? Once you can read the length and capacity of each slice, you can answer it before you run the code, and choose deliberately between sharing and copying.

What this helps you reason about

  • Predict whether a write through one slice shows up in another, after slicing and after append.
  • Spot an append that overwrites an element of another slice, and prevent it with s[a:b:c].
  • Explain why a function that appends must return the slice, and copy a slice when you need it independent.

In Java, Python, and JavaScript, a list is one growable object, and a second variable that refers to that object sees every change, including added elements. A Go slice is different: it is a small value that describes part of an array, and append returns a new slice value. After an append, two slices may share elements or may not, depending on their capacity. This lesson shows the capacity in the stepper so you can predict which.

Already know this? The short version
  • A slice value is a pointer into a backing array, a length, and a capacity; assignment, passing, and s[a:b] copy those three parts and share the elements.
  • s[a:b] has capacity cap(s) − a, so append on it can write into elements that a longer slice still uses; s[a:b:c] limits the capacity to c − a and makes the next append copy.
  • append writes in place when len + added ≤ cap and otherwise moves to a new array, after which the result shares nothing with the old slice; growth sizes are not specified.
  • A function that appends must return the new slice, and the code that called it must assign it, as with append itself.
  • copy(dst, src) copies min(len(dst), len(src)) elements, so dst needs a length; slices.Clone(s) returns an independent copy.
  • In real programs, watch for a small slice that keeps a large array in memory, appends into results that share one array, and slices.Delete setting elements to zero that another slice still shows: see “What goes wrong with slices in real programs?”.

Go to the final check· Go to the practice tickets

Words in this lesson (7)
slice
A small value with three parts: a pointer to elements stored elsewhere, a length, and a capacity. Copying a slice copies these parts, not the elements.
backing array
The array that holds a slice’s elements. Several slices can use one backing array, each using its own part of it.
length and capacity
A slice’s length, len(s), is how many elements it uses. Its capacity, cap(s), is how many elements fit in its storage before append must move them to new storage.
slice expression
s[a:b], which makes a new slice over the same backing array, starting at element a of s, with length b − a. No elements are copied.
three-index slice
s[a:b:c], a slice expression that also sets the capacity to c − a, so append cannot write into element c of s or any element after it.
append
A built-in function that returns a slice with new elements added at the end. It writes into the backing array when the capacity allows; otherwise it first copies the elements to a new, larger array.
slices.Clone
A function from the slices package (Go 1.21 and later) that returns a new slice with the same elements in a new backing array.

Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.

02 / Capacity

How far can a slice grow in place?

The length is how many elements a slice uses. The capacity is how many fit before the end of its backing array. The places between the two belong to the backing array, and append uses them first.

This is where slices surprise readers. head := all[:2] has length 2 and capacity 4. The two places after head are all[2] and all[3]. append(head, 9) has room, so it writes 9 into all[2]. all now reads [1 2 9 4], although no line wrote to all.

A three-index slice, s[a:b:c], also sets the capacity: it is c − a. all[:2:2] has length 2 and capacity 2. An append to it finds no room, so it copies the two elements to a new array first, and all keeps 3.

main.go
all := []int{1, 2, 3, 4}
// length 2, capacity 4
head := all[:2]
// room left: the 9 is written into all[2]
head = append(head, 9)
fmt.Println(all) // [1 2 9 4]

append writes into the capacity after a slice’s length, even when another slice uses those elements; s[a:b:c] limits that capacity.

Coming from Java: The closest Java form of all[:2] is all.subList(0, 2), a view that uses the same ArrayList. set through the view changes the list, as in Go. add through the view inserts into the list and moves the later elements along, so all becomes [1, 2, 9, 3, 4] and nothing is overwritten. If you add to the list itself instead, the view’s behavior is undefined by the List contract, and ArrayList’s view throws ConcurrentModificationException when it is used next. Go has neither protection: the append writes 9 into all[2], and nothing reports it.

Coming from JavaScript: In JavaScript, push always grows the one array that every name refers to; in Go, append through a short slice can overwrite an element that a longer slice still shows.

Precisely: three-index slices

In s[a:b:c], the indexes must satisfy 0 ≤ a ≤ b ≤ c ≤ cap(s). The first index can be left out, as in s[:2:2], but the second and third cannot. The specification calls this form a full slice expression, and it is not allowed on a string. The standard library uses this form for the same reason as this lesson: slices.Clip(s) returns s[:len(s):len(s)].

Experiment

Append to the first half of a slice

head := all[:2] has length 2 and capacity 4, because all’s array continues for two more elements. append(head, 9) has room, so it writes 9 into the next place of that array, which is all[2]. all[:2:2] sets the capacity to 2, so append finds no room and copies head’s two elements to a new array before adding 9.

Why it matters Before you append to a slice that was cut from a longer one, check its capacity. If the elements after it belong to someone else, limit the capacity with a third index.

Predict what all prints after the append to head. Then change all[:2] to all[:2:2] and predict again.

package main

import "fmt"

func main() {
    all := []int{1, 2, 3, 4}
    head := all[:2]
    fmt.Println(len(head), cap(head))
    head = append(head, 9)
    fmt.Println(all)
}
What to look for

Watch all while you step over head = append(head, 9). head grows from len: 2, cap: 4 to len: 3, cap: 4, and at the same stop all changes from [1,2,3,4] to [1,2,9,4], although no line names all. The run prints 2 4 and then [1 2 9 4]. After the edit, head starts as len: 2, cap: 2, the append gives head a new array (len: 3, cap: 4 in this run), all keeps [1,2,3,4], and the run prints 2 2 and then [1 2 3 4].

Expected output

2 4
[1 2 9 4]

03 / New array

When does append move to a new array?

When the new elements do not fit in the capacity, append makes a new, larger backing array. It copies the existing elements into the new array, adds the new ones, and returns a slice that uses the new array. The old slice still uses the old array.

After that, the two slices share nothing. A write through grown does not reach full. So the answer to “do these two slices share elements?” can change with one append, depending only on the capacity at that moment.

The Variables lesson showed the shared case: two appends to one slice with spare capacity printed 5 5. Here is the other case.

main.go
full := []int{1, 2}
// length 2, capacity 2: no room left
grown := append(full, 3)
grown[0] = 9
fmt.Println(full[0], cap(grown)) // 1 4

When the capacity is full, append copies to a new array, and the result stops sharing with the old slice.

Precisely: how much the capacity grows

The specification says only that append allocates a new, sufficiently large array when the capacity is not enough. How much larger is a choice of the runtime. The capacities in this lesson (2 to 4, 3 to 6) were observed with Go 1.27, the stepper’s version. The runtime roughly doubles small slices, grows larger ones by a smaller factor, and rounds up to the memory allocator’s size classes. Code should not depend on these numbers.

Experiment

Append until the capacity runs out

start has length 2 and capacity 3. append(start, 7) fits, so more shares start’s array. append(more, 8) needs a fourth place and capacity 3 has none, so append makes a new array, copies 1, 0, 7 into it, and adds 8. From then on most and more use different arrays. With capacity 4, the second append fits too, and all three slices share one array.

Why it matters Whether two slices still share elements after append depends on the capacity, not on the code you see. Do not rely on either outcome: keep one result, as in s = append(s, v), or make the copy explicit.

Predict both lines of output. Then change make([]int, 2, 3) to make([]int, 2, 4) and predict again.

package main

import "fmt"

func main() {
    start := make([]int, 2, 3)
    more := append(start, 7)
    more[0] = 1
    most := append(more, 8)
    most[0] = 2
    fmt.Println(start[0], more[0], most[0])
    fmt.Println(cap(start), cap(more), cap(most))
}
What to look for

Step over each line and compare the three slices. After more[0] = 1, start shows [1,0] as well: more still uses start’s array. most appears with len: 4, cap: 6, and after most[0] = 2, start and more keep 1. The run prints 1 1 2 and 3 3 6. After the edit, most has len: 4, cap: 4, most[0] = 2 changes all three, and the run prints 2 2 2 and 4 4 4.

Expected output

1 1 2
3 3 6

04 / Functions

Why must a function return the slice it appended to?

A slice parameter is a copy of the caller’s slice value. Inside the function, boxes = append(boxes, weight) changes the function’s copy: its length, and its pointer if append moved to a new array. The caller’s slice value is not changed.

With spare capacity, the new element is written into the caller’s backing array, past the caller’s length, so the caller does not see it. Without spare capacity, the element goes into a new array that the caller never sees. In both cases the caller’s length stays the same.

That is why append returns a slice, and why a function that appends should do the same. The code that called it must store the result.

main.go
func addBox(boxes []int, weight int) []int {
    return append(boxes, weight)
}

boxes := []int{3}
boxes = addBox(boxes, 7)
fmt.Println(boxes) // [3 7]

A function that appends returns the slice, and the code that called it assigns the result.

Coming from Java: list.add(x) inside a Java method changes the one ArrayList the caller holds; in Go, the caller’s slice keeps its old length until the caller assigns the returned slice.

Precisely: a pointer to a slice

A function can also take a pointer to the caller’s slice, as in func addBox(boxes *[]int, weight int) { *boxes = append(*boxes, weight) }, called as addBox(&boxes, 7). Writing *boxes = changes the caller’s slice value itself. Returning the slice is more common, and it matches the standard library, where append, slices.Insert, and slices.Delete all return the new slice.

Experiment

Append inside a function

boxes in addBox is a copy of main’s slice value. append returns a new slice value with length 2, and the assignment changes addBox’s copy only. With spare capacity, the 7 is written into the array the two slices share, but main’s length is still 1, so main does not see it. Without spare capacity, the 7 goes into a new array that main never sees.

Why it matters A function that appends must return the slice, and the code that called it must store the result: boxes = addBox(boxes, 7). That is why append itself returns a slice.

Predict both lines. boxes[:cap(boxes)] shows every element up to the capacity, including the ones past the length. Then change make([]int, 1, 4) to make([]int, 1, 1) and predict again.

package main

import "fmt"

func addBox(boxes []int, weight int) {
    boxes = append(boxes, weight)
    fmt.Println("inside", len(boxes), cap(boxes))
}

func main() {
    boxes := make([]int, 1, 4)
    addBox(boxes, 7)
    fmt.Println("caller", len(boxes), boxes[:cap(boxes)])
}
What to look for

Step into addBox and select each frame in turn. After the append, addBox’s boxes shows len: 2, cap: 4, [0,7], while main’s boxes still shows len: 1, cap: 4, [0]. The run prints inside 2 4 and then caller 1 [0 7 0 0]. main’s boxes has length 1, so the 7 shows only because boxes[:cap(boxes)] reaches past that length into main’s array. After the edit, addBox’s boxes shows len: 2, cap: 2 (a new array), main’s boxes keeps len: 1, cap: 1, and the run prints inside 2 2 and then caller 1 [0].

Expected output

inside 2 4
caller 1 [0 7 0 0]

05 / Copies

How do you get a slice that shares nothing?

To get a slice that shares nothing, put the elements in a new backing array.

copy(dst, src) copies elements from src into dst and returns how many it copied. It copies as many as the shorter length allows: lengths, not capacities. So dst needs a length. make([]int, 0, 3) has room for three elements but length 0, and copy into it copies nothing. make([]int, len(src)) has length 3.

slices.Clone(src) does both steps: it makes a new backing array and copies the elements into it.

A slice declared with var s []int is nil. A slice written []int{} is empty but not nil. Both have length 0, both work with len, range, and append, and only s == nil tells them apart. The difference shows when data leaves the program: encoding/json writes a nil slice as null and an empty one as [].

main.go
src := []int{4, 8, 15}
dst := make([]int, len(src))
n := copy(dst, src)
backup := slices.Clone(src)
src[0] = 0
// 3 [4 8 15] [4 8 15]
fmt.Println(n, dst, backup)

copy fills the length of dst; slices.Clone makes a new backing array with the same elements.

Precisely: what Clone and nil change

Both copy and slices.Clone copy one level only: if the elements are slices, pointers, or maps, the copies still share what those refer to. slices.Clone may return extra capacity: cloning five ints gave capacity 6 in the stepper’s Go 1.27, which is harmless because that array belongs to the clone alone. slices.Clone of a nil slice returns nil, and of an empty, non-nil slice returns an empty, non-nil slice. Older code writes append([]int(nil), src...) instead, which differs in one case: for an empty, non-nil src it returns nil, so encoding/json writes null where the original gave [].

06 / In production

What goes wrong with slices in real programs?

The rules above explain four bugs that Go programmers meet in real programs.

A small slice keeps its whole backing array in memory. If a program reads a 10 MB file into data and stores data[:16] in a map, all 10 MB stay allocated while that entry exists. Store slices.Clone(data[:16]) instead.

Every holder of one slice can append into the same spare capacity: results collected in a [][]T, struct fields, map entries. Below, both stored paths end up as [1 3]. Append to path[:len(path):len(path)], or clone each result before you store it, so that every stored path gets its own array.

slices.Delete, slices.Compact, and slices.Replace change the backing array in place, and since Go 1.22 they set the elements past the new length to zero values. Another slice that still has the old length shows those zeros, as the experiment below shows.

When you return part of a slice to code that you do not control, return slices.Clip(part), which is part[:len(part):len(part)]. An append by that code then copies first, instead of writing into the rest of your array.

main.go
path := make([]int, 0, 4)
path = append(path, 1)
var found [][]int
found = append(found, append(path, 2))
found = append(found, append(path, 3))
fmt.Println(found) // [[1 3] [1 3]]

Before you keep, store, or return a slice, ask who else uses its backing array: Clone what you keep, and Clip what you return.

Experiment

Delete from a slice that another slice still uses

slices.Delete works in the backing array of ids. It moves 13 and 14 down one place, returns a slice of length 3 over the same array, and, since Go 1.22, sets the element after that length to the zero value, 0. ids still has length 4, so it shows that 0. With slices.Clone(ids), all of this happens in the clone’s array.

Why it matters After slices.Delete, Compact, or Replace, use only the slice they return. If another part of the program still reads the original slice, delete from a clone.

Predict both lines. Then change slices.Delete(ids, 1, 2) to slices.Delete(slices.Clone(ids), 1, 2) and predict again.

package main

import (
    "fmt"
    "slices"
)

func main() {
    ids := []int{11, 12, 13, 14}
    kept := slices.Delete(ids, 1, 2)
    fmt.Println(kept)
    fmt.Println(ids)
}
What to look for

Step over the line with slices.Delete. At the next stop, kept appears with len: 3, cap: 4, [11,13,14], and ids changes from [11,12,13,14] to [11,13,14,0], although the line only names ids as an argument. The run prints [11 13 14] and then [11 13 14 0]. After the edit, ids keeps [11,12,13,14] at every stop, and the second line is [11 12 13 14].

Expected output

[11 13 14]
[11 13 14 0]

07 / Try it

Can you predict which slices share?

Two slices are cut from one array, and one of them grows twice. Follow the length and capacity of left after each append.

main.go
package main

import "fmt"

func main() {
    base := []int{1, 2, 3, 4, 5}
    left := base[:2]
    right := base[2:]
    left = append(left, 0)
    fmt.Println(right[0], len(left), cap(left))
    left = append(left, 7, 7, 7)
    left[0] = 9
    fmt.Println(base[0], right[0], cap(right))
}

What are the two lines of output?

Reveal the reasoning

0 3 5, then 1 0 3. left has length 2 and capacity 5, so the first append writes 0 into base[2], which is right[0]. The second append needs length 6, more than the capacity 5, so left moves to a new array; left[0] = 9 changes only that new array, and base[0] stays 1. right still starts at base[2], with capacity 3.

Practice tickets

  1. GO-101 · Undo brings back an empty route → Free
  2. GO-102 · Every ride chart comes out empty → Sign in · Pro
  3. GO-103 · The change report loses its last student → Sign in · Pro

Keep exploring

Stack and heap →

Primary references