Skip to content
Theory · Arrays and slices · lesson 4 of 10

append and make

The literal []int{1, 2, 3} fits when the elements are known while writing the code. Even numbers from input or found words are collected one by one while the program runs. The window onto the lockers then has to grow.

The built-in append function adds a new element to the end of a slice:

GO
package main

import "fmt"

func main() {
    var s []int
    fmt.Println(len(s), s == nil) // 0 true
    s = append(s, 1)
    s = append(s, 2, 3)
    fmt.Println(s, len(s)) // [1 2 3] 3
}

Let's walk through the example line by line:

  1. var s []int declares a slice with no elements, so the first Println prints 0 true.
  2. s = append(s, 1) returns the slice [1], and we assign it back into s.
  3. s = append(s, 2, 3) adds two values to the end at once.
  4. The last Println prints [1 2 3] 3.

The nil slice

The zero value of a slice is nil: a window that is not looking at any row yet. Such a slice has three properties:

  • it has no elements, and len returns 0;
  • fmt.Println prints it as [];
  • append works with it, so a declaration var s []int is enough for collecting values.

You can check whether the window is empty with s == nil. It is the only comparison Go allows for a slice.

Calling append

The call append(s, v) returns a slice in which v follows the last element of s. Let's look at its parts:

  • the first argument is the slice being added to;
  • the other arguments are the new elements, and one call can add several: append(s, 2, 3);
  • the result is a slice, which is assigned back into the same variable.

A slice of the right length

When the length is known in advance, the slice is created with the built-in make function. It has two forms.

Length: make([]int, 3)

The call make([]int, 3) creates a slice of three zeros:

  • []int is the slice type;
  • 3 is the length, the number of elements.

The elements can be changed by index right away. These zeros are ordinary elements: append adds a new value after them, not in place of the first zero.

Length and capacity: make([]int, 0, 5)

A third argument sets the capacity—how many elements fit in the slice in total, counting the ones already created. In our picture it is the number of lockers from the window's left edge to the end of the row. The call make([]int, 0, 5) creates an empty slice with room for five elements, and cap(b) returns that number.

Both forms side by side:

GO
a := make([]int, 3)
a[1] = 7
fmt.Println(a) // [0 7 0]
b := make([]int, 0, 5)
fmt.Println(len(b), cap(b)) // 0 5
  1. a holds three zeros, and a[1] = 7 changes the middle one, so it prints [0 7 0].
  2. In b the window shows no lockers at all, but the row has five: it prints 0 5.

How a slice is built

A slice holds three values, and together they are called the slice header:

  • a reference to the array where the elements live—this array is called the backing array;
  • the length;
  • the capacity—the number of elements of the backing array, counting from the start of the slice.

What append does depends on whether there is room left.

Capacity is enough

While the length is less than the capacity, append writes the new element into the same array, and the window widens by one locker. Nothing is copied.

Capacity runs out

When there is no room, append works in three steps:

  1. It creates a larger array.
  2. It copies the old elements into it.
  3. It returns a slice that points to the new array.

The new array is taken with spare room, so the following append calls again write without copying. How much larger it is is up to Go, and the exact factor depends on the version and on the slice's size.

Press the button and watch when append writes into the same array and when it moves the elements to a new one.

LiveSlice: appendlen = 2 · cap = 2 · array 1
← the array is full

Slice: length 2, capacity 2 — no spare room. Press the button.

Watch the array number

This is where the locker analogy ends: nobody moves real belongings to a new row by themselves. append does move them, and the old window keeps looking at the old row.

A slice from make([]int, 0, 5) has capacity 5: the first five append calls write into one array, and the sixth creates a new one. A variable that was not assigned the result still holds the old length.

Interviewers like to ask what a slice is and how append grows it. The answer has three parts:

  • a slice is a header of a reference to a backing array, a length and a capacity;
  • while there is free capacity, append writes in place; when there is none, it creates a larger array and copies the elements into it;
  • the header after the call may be different, so the result is always assigned: s = append(s, x).

Common mistake

The result of append was not assigned:

GO
s := []int{1, 2, 3}
append(s, 4) // ✗
TEXT
append(s, 4) (value of type []int) is not used

The compiler rejects such a call: append does not change the variable s, it only returns a new slice. Write s = append(s, 4).

Key points

  • append returns a new slice, and the result is assigned: s = append(s, v).
  • make([]int, n) creates n zero elements, and make([]int, 0, c) creates an empty slice of capacity c.
  • A slice is a header of a reference to a backing array, a length and a capacity.
  • When capacity runs out, append creates a larger array and copies the elements into it.

Subscribe to take the course

The course is free. Sign in with Telegram and subscribe to the channel to open every lesson.

Sign in another way