Two list types, one you will actually use
Go has two list-like types that look almost the same. An array has a fixed size that is part of its type: [3]int holds exactly three ints and can never hold four. A slice, written []int with nothing between the brackets, is a window onto an array that Go manages for you. Almost all everyday Go code uses slices.
The size is the whole difference in the type. [3]int and [4]int are different types, so you can't assign one to the other. Every []int is the same type, whatever it currently holds.
Length and capacity
A slice has two measurements:
len(s)is how many elements you can see right now.cap(s)is how many elements fit in the backing array, counting from the slice's first element, before Go must allocate a bigger one.
A literal such as []int{10, 20, 30} creates a backing array of exactly three elements, so both numbers are 3. The array is full.
Growing with append
append adds elements to the end. When there is spare capacity it writes into the existing array. When the array is full, it allocates a larger one, copies the elements over and returns a slice that points to the new array. Either way, the result is what you must keep:
nums := []int{10, 20, 30}
nums = append(nums, 40)
fmt.Println(len(nums), cap(nums)) // 4 6
Forgetting to assign the result back is a common mistake: append(nums, 40) on its own leaves nums unchanged (and the compiler rejects an unused result). With the Go 1.22 runtime, growing a full slice of three ints makes room for six, which is why the capacity jumps from 3 to 6. Treat the exact growth numbers as an implementation detail and never depend on them in real programs. The lesson prints them only to show that a new array was allocated.
Trap 1: arrays don't grow
If you write the size by accident, append refuses to compile:
arr := [3]int{10, 20, 30}
arr = append(arr, 40)
first argument to append must be a slice; have arr (variable of type [3]int)
The fix is almost always to remove the number so that you have a []int. Arrays also behave differently when copied. Assigning an array to another variable copies all of its elements, while assigning a slice copies only the small header that points to the shared backing array:
arr := [3]int{10, 20, 30}
b := arr
b[0] = 1
fmt.Println(arr, b) // [10 20 30] [1 20 30]
a := []int{1, 2, 3}
c := a
c[0] = 100
fmt.Println(a, c) // [100 2 3] [100 2 3]
Trap 2: slicing shares memory
The expression s[a:b] copies nothing. It returns a new slice that views elements a up to, but not including, b of the same backing array. So nums[1:3] has two elements, and writing through it changes the original:
window := nums[1:3]
window[0] = 99 // the same memory as nums[1]
Nothing warns you, and a value you never touched in nums changes. This is called aliasing. When you need an independent copy, make one on purpose:
safe := make([]int, len(nums))
copy(safe, nums)
safe[0] = -1 // nums is not affected
Capacity adds a second wrinkle. A slice's capacity can extend past its length, and append uses that spare room without allocating. Two slices that share an array can then overwrite each other's appended values:
base := make([]int, 3, 10)
w := append(base, 7)
w2 := append(base, 8)
fmt.Println(w, w2) // [0 0 0 8] [0 0 0 8]
Both appends wrote into the same slot, so the second overwrote the first. Reslicing with a full slice expression such as nums[1:3:3] caps the capacity at the end of the window, which forces the next append to allocate a new array instead.
The exercise, step by step
- Create the slice.
nums := []int{10, 20, 30}with empty brackets, so it can grow. - Print the length and capacity. Pass the labels and numbers as separate arguments to
fmt.Println, which separates arguments with a space. - Append 40 and keep the result by assigning it back to
nums. - Print both numbers again. The length is now 4 and the capacity 6, proof that a bigger array was allocated.
- Take a window with
nums[1:3]. Index 3 is excluded, so the window holds 20 and 30. - Print the window to confirm it is
[20 30]. - Assign 99 to
window[0]. No:=is needed, since this changes an existing element. - Print
nums. The 99 appears at index 1, although you never wrote tonumsdirectly.
The solution in Go
nums := []int{10, 20, 30}
fmt.Println("len =", len(nums), "cap =", cap(nums))
nums = append(nums, 40)
fmt.Println("after append: len =", len(nums), "cap =", cap(nums))
window := nums[1:3]
fmt.Println("window =", window)
window[0] = 99
fmt.Println("nums =", nums)
The output is:
len = 3 cap = 3
after append: len = 4 cap = 6
window = [20 30]
nums = [10 99 30 40]
Note the order of the steps. The window is taken after the append, so it points into the new, larger array, and nums still shares that array. Had the window been taken before the append, the append would have moved nums to a new array, and writing to the window would no longer have changed it.
Key takeaways
- An array's size is part of its type and can't change. A slice is a view with a length and a capacity.
- Always keep the result of
append:nums = append(nums, x). - Slicing with
s[a:b]copies nothing. Writes through the new slice show up in the original. - Use
copyinto a fresh slice when you need independent data. - If the compiler says
first argument to append must be a slice, you wrote[3]intwhere you wanted[]int.