append

append is Go's built-in function for growing a slice. You call it with a slice and one or more values to add, and it returns a new slice with those values at the end. This is how you build up a collection in everyday Go code.

The basic shape

nums := []int{1, 2}
fmt.Println(nums, len(nums), cap(nums))   // [1 2] 2 2

nums = append(nums, 3)
fmt.Println(nums, len(nums), cap(nums))   // [1 2 3] 3 4

nums = append(nums, 4, 5)
fmt.Println(nums, len(nums), cap(nums))   // [1 2 3 4 5] 5 8

Two things to notice straight away.

First, append returns a new slice. You have to assign the result back to a variable; the original slice is not modified in place. More on why in a moment.

Second, append is variadic (which you met in the Functions chapter). You can pass one value, several at once, or spread another slice with ...:

extras := []int{6, 7, 8}
nums = append(nums, extras...)
fmt.Println(nums)   // [1 2 3 4 5 6 7 8]

The extras... syntax is the same spread you used to forward a slice into a variadic function.

Always assign the result

This is the #1 bug Go newcomers hit with slices. If you call append and throw away the result, your "addition" disappears:

nums := []int{1, 2}

append(nums, 3)       // the returned slice is discarded
fmt.Println(nums)     // [1 2]   (unchanged!)

append returns the slice value that knows about the new length. Sometimes that returned slice still refers to the same backing array. Sometimes it refers to a bigger one. Either way, if you ignore the result, you keep the old slice value, so your variable still looks unchanged.

You can see the visible effect here:

a := []int{1, 2}
b := append(a, 3)

fmt.Println(a)   // [1 2]
fmt.Println(b)   // [1 2 3]

b is the slice that includes the new element. a is still a 2-element slice. If you had written append(a, 3) and thrown the result away, there would be nowhere to see the 3 from. That is why the rule is blunt: always write s = append(s, ...). The compiler will not complain if you forget, so forming the habit is the only defence.

Gotcha

The compiler does not warn when you drop the append result. Type s = append(s, ...) every time, without thinking about it. Static-analysis tools like go vet catch some shapes of this bug on your own machine, but not all of them, so the habit is the real fix.

When append reallocates

append into a slice with room to spare (length less than capacity) simply writes into the next position and bumps the length:

nums := make([]int, 0, 4)   // len 0, cap 4
nums = append(nums, 10)      // len 1, cap 4
nums = append(nums, 20)      // len 2, cap 4
fmt.Println(len(nums), cap(nums))   // 2 4

Once the slice is full (length equals capacity), the next append allocates a bigger underlying array, copies every existing element into it, and returns a slice that refers to that new array:

nums = append(nums, 30, 40)                // len 4, cap 4 (still fits)
fmt.Println(len(nums), cap(nums))          // 4 4

nums = append(nums, 50)                    // now the slice grows
fmt.Println(len(nums), cap(nums))          // one run: 5 8

The exact capacity jump size is not the point and is not something to rely on. What matters is that capacity grows in chunks, and once the slice runs out of room, append has to allocate a larger backing array and copy the existing elements across. If you know roughly how big the slice will end up, pre-allocating with make([]T, 0, n) from the previous lesson avoids the repeated copy-and-grow cycle entirely.

Task

The starter declares nums := []int{1}. Extend the program to:

  • Append the single value 2 to nums, assigning back.
  • Append the two values 3, 4 in one call.
  • Declare a slice more := []int{5, 6, 7} and append its contents to nums using the spread operator.
  • Print nums. You should see [1 2 3 4 5 6 7].
Expected output
[1 2 3 4 5 6 7]