Shanraq.org Shanraq.org
Arrays and slices in Go: a list that grows, append, len and cap
IT

Go: from zero to your own blog Lesson 8 of 50

Arrays and slices in Go: a list that grows, append, len and cap

The eighth lesson of the Go course. The array a slice is made of, and the slice itself: []string, append, and why the result must always be assigned back. How an array differs from a slice on assignment, how length differs from capacity, and the trap that matters most — a piece of a slice looks into the same memory as the original.

Why this matters

So far one name has held one value. But a blog has more than one article, a post has several tags, and the comments under it keep coming. You need a list, and in Go it is called a slice.

Along the way for delivers the second half of its promise: the range you took over a string will now walk a list.

The whole thing at once

A new folder, go mod init sabaq07, main.go:

package main

import "fmt"

func main() {
	var titles []string

	titles = append(titles, "Shanyraq")
	titles = append(titles, "The Go language")
	titles = append(titles, "The steppe")

	fmt.Println("articles:", len(titles), "· room:", cap(titles))

	for i, t := range titles {
		fmt.Println(i+1, t)
	}

	fmt.Println("first two:", titles[:2])
	fmt.Println("last:", titles[len(titles)-1])
}

Before you run it, and without looking below, say out loud what it will print. Then run it and compare: the difference is exactly where you do not know the language yet.

go run . prints:

articles: 3 · room: 4
1 Shanyraq
2 The Go language
3 The steppe
first two: [Shanyraq The Go language]
last: The steppe

Notice: three articles, but room for four.

Taking it apart

First the array: what a slice is made of

The word “array” has just come up — let us finish the thought, because a slice cannot be explained without it.

An array in Go is a list whose length is fixed once and for all and belongs to the type itself. [3]string and [4]string are different types, and one cannot be assigned to the other. That is why ordinary code hardly ever writes an array: knowing the number of elements in advance is rare.

One difference has to be known, because everything else follows from it. An array is copied when it is assigned; a slice is not:

titles := [3]string{"Shanyraq", "The Go language", "The steppe"}
copyOf := titles
copyOf[0] = "The mountains"
fmt.Println("array:", titles[0], "·", copyOf[0])

list := []string{"Shanyraq", "The Go language", "The steppe"}
same := list
same[0] = "The mountains"
fmt.Println("slice:", list[0], "·", same[0])
array: Shanyraq · The mountains
slice: The mountains · The mountains

The array was assigned, and a second independent triple of strings came out of it: changing one leaves the other alone. The slice was assigned, and both names look at the same array, so an edit made through one name shows through the other.

Which is what a slice really is: three numbers — the address of an array, a length and a capacity. The data sits in the array; the slice only says how to reach it. That is also where append gets its habit of “moving house”: the room in the array ran out, so a bigger array is taken.

You need not count the elements by hand: [...]string{"Shanyraq", "The Go language", "The steppe"} is an array too, and the compiler counts its length.

Where an array does turn up: wherever the length is part of the meaning. A SHA-256 hash is a [32]byte, and no other length is possible. There the array states the size in the type itself.

[]string — a slice of strings

The square brackets in front of the type are the slice. There is nothing inside them: no length is fixed in advance, the list grows as needed.

var titles []string

Such a slice is empty, and that is a legal state: len is zero, cap is zero, and the value itself is nil. You can append to it straight away — no preparation required.

append adds, and returns a new slice

titles = append(titles, "The steppe")

The same name stands on both sides, and that is not a typo. append does not change the old slice; it returns a new one. If there was room, it writes the element into the same array and returns a slice with a new length; if there was not, it takes a larger array, and the slice then lives at a different address. Forget to assign it back and the addition is lost, and the program will not even complain.

Picture it. Moving house. You ask for one more thing to be put away, and you are told: “it fit, but we had to move, here is the new address.” Fail to write the address down and you keep going to the old place, wondering why everything is still as it was.

This is the mistake everyone makes exactly once. After that the hand types x = append(x, …) on its own.

Length and capacity

len is how many elements are there. cap is how many will fit before a move is needed.

In the example they are three and four: Go took the room with some to spare, so it does not have to relocate the list on every addition. Do not memorise the four: how much spare room to take is up to the implementation, and another version of Go may give a different number. len is what you check; cap is only something to watch.

Picture it. A box on a shelf. Length is how many books are in it right now. Capacity is how many will fit before you have to fetch a bigger box and move everything across.

Knowing the capacity is useful, but managing it by hand almost never is: Go grows it on its own and with room to spare — a small slice doubles, and beyond that it grows more cautiously. The language does not promise the exact new capacity, and a program must not rely on it. What matters is the other part: a thousand appends cost about ten moves rather than a thousand.

Indexes and bounds

The first element is titles[0], the last is titles[len(titles)-1]. Counting starts at zero.

Going out of bounds is not forgiven: titles[5] on three elements stops the program with index out of range. That is better than quietly handing back rubbish — you learn about the mistake that same second, not a week later from a reader.

A piece of a slice

titles[:2]    // the first two
titles[1:]    // from the second to the end
titles[1:3]   // from the second up to the third

The first number is where from, the second is up to which one, not including it. A missing number means “from the start” or “to the end”.

The trap: a piece is not a copy

Here is what this section is for:

two := titles[:2]
two[0] = "CHANGED"
fmt.Println(titles[0])   // CHANGED

We edited two, and titles changed. A piece of a slice is not a separate list but a window into the same memory.

Picture it. Not a photocopy of a document but a window into the same warehouse. Through the window you see part of the shelving, and if you move something through the window, it has moved in the warehouse itself.

When you need a real copy, make it explicitly:

two := make([]string, 2)
copy(two, titles[:2])

For now hold on to one thing: a slice taken from a slice shares its memory. Most of the time this does not get in your way, but the day something “changed for no reason”, remember this paragraph.

range over a list

for i, t := range titles {

The same as over a string, only simpler: i is the position counting from zero, with no bytes involved, and t is the element itself. When you do not need the position, an underscore takes its place: for _, t := range titles.

The lesson map

The lesson map: length three, capacity four

Say it in your own words

Without looking, answer out loud or on paper. The answers are at the end of the lesson.

  1. Why is append written as x = append(x, v) rather than just append(x, v)?
  2. How does length differ from capacity, and what is the second one for?
  3. You took part := titles[:2] and edited part[0]. What happened to titles[0], and why?

Exercise

Required. Write a function longer(titles []string, n int) []string that returns only the titles with more than n letters. Count letters, not bytes, as in the previous lesson. Call it on the list "The steppe", "Shanyraq", "Go", "Көш" with n = 3 and print the result.

Optional.

  • A function total(nums []int) int that adds up every number in a list.
  • Append ten titles in a loop and print len and cap after each one. At which step does the capacity change?
  • Make a real copy of the first two titles with make and copy, edit the copy, and check that the original is intact.

Where this goes in your blog

The list of articles on the front page, the tags under a title, the comments under the text, the search results — all of them are slices. In the templates lesson you will hand such a list to HTML and it will unfold into cards; in the database lesson you will get it back from there in a single query.

Answers

Show the answers
  1. Because append returns a new slice instead of changing the old one. When there is not enough room it relocates the data and the address changes; without the assignment you are left with the old value and the addition is lost. The compiler will not warn you: it sees a function call whose result was not taken.
  2. Length is how many elements there are now, capacity is how many fit before a move. The second one exists so that an addition does not relocate the list every single time: Go takes room to spare and grows it on its own, so a thousand appends cost about ten relocations. By how much it grows is not something the language promises.
  3. titles[0] became “CHANGED” as well. A piece of a slice does not copy the data, it looks into the same memory, so an edit through one is visible through the other. A copy is made explicitly, with make and copy.

Sources

If you have found a mistake or a typo in this article, tell us about it

Check your exercise

Solve it and run it in VS Code first — the editor shows the mistake where you made it. Paste the finished solution here. A model reads it: it will point at the mistake but will not hand you the answer.

Sign in to have it checked. Sign in

Comments (0)

No comments yet. Be the first.