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
Say it in your own words
Without looking, answer out loud or on paper. The answers are at the end of the lesson.
- Why is
appendwritten asx = append(x, v)rather than justappend(x, v)? - How does length differ from capacity, and what is the second one for?
- You took
part := titles[:2]and editedpart[0]. What happened totitles[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) intthat adds up every number in a list. - Append ten titles in a loop and print
lenandcapafter each one. At which step does the capacity change? - Make a real copy of the first two titles with
makeandcopy, 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
- Because
appendreturns 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. - 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.
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, withmakeandcopy.
Sources
If you have found a mistake or a typo in this article, tell us about it
Comments (0)
Log in to leave a comment →
No comments yet. Be the first.