Go from the ground up

Slices and maps

The two collections every Go program uses: a slice is a growable list, a map is a lookup by key, and both of them have one behaviour worth learning exactly.

Part 4 of 12

Beginner15 min readBack to Courses

A slice is a window on an array

A slice is the list you will use: prices := []float64{19.5, 4.0}. make([]string, 0) starts an empty one with a known job, and append adds to the end. The subtle part is that append returns the slice — it may have moved it to a bigger array — so you always write names = append(names, "Ada"), never just append(names, "Ada").

Slices have a length and a capacity: the length is how many items there are, the capacity is how many fit before the next growth. len is the one you will use constantly; the capacity is why appending to a slice somebody else holds can surprise you, and why a copy — slices.Clone, or append([]T(nil), s...) — is sometimes the honest thing to do.

A map is a lookup

A map holds a value per key: ages := map[string]int{"ada": 36}, or make(map[string]int) to start empty. Assignment writes a key; reading a key that is not there gives the zero value, which is a fact worth remembering — for a map of int, a missing key reads as 0, and 0 is a perfectly real answer.

That is what the comma-ok form is for: age, ok := ages["ada"] tells you whether the key existed, separately from the value it had. delete(ages, "ada") removes a key, and iterating with range gives keys in no particular order — deliberately, so no program can quietly depend on an order the map never promised.

nil is usable, until it is not

A declared map or slice starts as nil, and both are readable in that state: len answers 0, range walks nothing. Writing to a nil map panics, though, so a map you intend to fill is made with make or a literal first. A nil slice, on the other hand, takes append happily — which is why returning a slice from a function needs no special care.

Reading is where nil earns its keep: an empty result needs no ceremony, and the zero value of the collection is already the shape of "nothing here".

Growing a list, counting with a map

package main

import (
	"fmt"
	"sort"
	"strings"
)

func main() {
	var names []string
	names = append(names, "ada", "grace", "alan")
	fmt.Println("names:", names, "count:", len(names))

	words := strings.Fields("the quick the lazy the")
	counts := make(map[string]int)
	for _, word := range words {
		counts[word]++
	}

	if n, ok := counts["the"]; ok {
		fmt.Println("the appears", n, "times")
	}
	if _, ok := counts["missing"]; !ok {
		fmt.Println("missing is not in the map — and reading it would give", counts["missing"])
	}

	keys := make([]string, 0, len(counts))
	for word := range counts {
		keys = append(keys, word)
	}
	sort.Strings(keys)
	fmt.Println("words:", keys)
}

Count the words, rank the scores

Run it, then change the map to count total letters per word instead of how often it appears, and print only the words longer than three characters.

Output
Click "Run" to execute your code

The zero value is a real answer. counts[word]++ works on a map of counts precisely because a missing key reads as 0 — and if you ever need to tell "absent" from "zero", that is exactly what the comma-ok form is for.

A slice is a header, not a list

A slice value is three words: a pointer to its first element, a length, and a capacity. Copying a slice copies those three words, not the elements — which is why two slices can look independent and still write to the same array.

append uses the spare capacity if there is any. When there is none, it allocates a bigger array, copies the old elements across, and returns a header pointing at the new one. The old header still points at the old array, so an append through one copy is invisible through another.

A map is a pointer to a hash table. Copying a map copies that pointer, so both names see every write. make(map[string]int) allocates the table; a nil map has none, and a write to nil panics while a read quietly returns the zero value.

Growing, in numbers

A worked growth shows why capacity matters. Start with var s []int; its length and capacity are both 0. The first append allocates a small array — capacity 1 — and returns a header for it. Appends then grow the capacity in steps: small slices roughly double, so you will see 1, 2, 4, 8, 16 early on. If you append five items to a slice with capacity 4, the fifth allocates a new array, copies the four, and returns a header pointing at the new one. Any other slice still pointing at the old array keeps the old four values, which is exactly the aliasing surprise from the warning below. make([]int, 0, 100) skips the repeated growth when the final size is known, and copy(dst, src) duplicates elements explicitly when a clone is not available.

Two slices, one array: with first := []int{1, 2, 3} and second := first[:2], writing second[0] = 99 changes first[0] too. Copy with append([]int(nil), first...) when independence is the point. And a map assignment is still a write on a nil map — check that make or a literal came first.

Try these next

  • Print len and cap after each append while growing a slice from empty to ten items.
  • Make a sub-slice, change one element through it, and print the original to confirm they share the array.
  • Read a missing key from a nil map, then assign to it and observe the panic.

Stock, counted two ways

Run it, then add a fourth order line and a new product. Predict the ink count before you run, and explain it from the map copy.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

Why is it names = append(names, "Ada") and not just append(names, "Ada")?