Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

7. Slices, Maps, and Strings

Three types carry nearly all the data in a Go program. None of them is complicated, and each has one behavior that will surprise you once.

7.1 Arrays

An array’s length is part of its type. [3]int and [4]int are different types, a function taking one will not accept the other, and the length must be a constant. Arrays are values, so assigning one copies every element, as section 5.1 showed.

var a [3]int              // three zeros
b := [3]string{"x", "y"}  // third is ""
c := [...]int{1, 2, 3, 4} // length 4, counted by the compiler

You will rarely declare one. Arrays exist so that slices can be built on them, and they appear directly in the places where a fixed size is the point, like a [32]byte hash.

7.2 Slices

A slice is a view into an array: a pointer to an element, a length, and a capacity. []int is a slice of ints, with no length in the type.

// Length, capacity, and what append does when it runs out.
package main

import "fmt"

func main() {
	var s []int // nil, but usable
	fmt.Println(s == nil, len(s), cap(s))

	for i := range 5 {
		s = append(s, i)
		fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
	}

	t := make([]int, 3, 10) // length 3, capacity 10
	fmt.Println(len(t), cap(t), t)

	u := s[1:3] // elements 1 and 2
	fmt.Println(u, len(u), cap(u))

	v := make([]int, len(s))
	copy(v, s) // an independent copy
	v[0] = 99
	fmt.Println(s, v)
}
true 0 0
len=1 cap=1 [0]
len=2 cap=2 [0 1]
len=3 cap=4 [0 1 2]
len=4 cap=4 [0 1 2 3]
len=5 cap=8 [0 1 2 3 4]
3 10 [0 0 0]
[1 2] 2 7
[0 1 2 3 4] [99 1 2 3 4]

The zero value of a slice is nil, and a nil slice is a working empty slice: len is 0, range runs zero times, and append works. You almost never need to initialize a slice before appending to it, and you should compare against len(s) == 0 rather than s == nil, because an empty non-nil slice is a different value that behaves identically.

append returns a new slice and you must use the result. When the existing capacity has room, it writes into the array already there and hands back a longer view. When it does not, it allocates a bigger array, copies, and hands back a slice of that. The growth above went 1, 2, 4, 4, 8; the exact schedule is not specified and has changed between releases, so rely on it being amortized constant time and not on any particular number.

make([]T, len, cap) allocates a slice of a given length with room to grow. When you know roughly how many elements are coming, make([]int, 0, n) and then appending avoids every intermediate copy, and it is the one slice optimization worth doing by reflex.

s[i:j] takes elements i up to but not including j, either index omissible. The result shares storage with s, so writing through one is visible through the other, and its capacity runs to the end of the underlying array, so s[1:3] above reports a capacity of 7. This is also a way to leak: a small slice of a large array keeps the whole array alive. copy(dst, src) copies the overlap of the two and returns how many elements moved, and it is how you get an independent copy.

Now the surprise.

// append may or may not share storage with the slice it was given.
package main

import "fmt"

func main() {
	base := make([]int, 3, 8) // room to spare
	a := append(base, 10)
	b := append(base, 20) // reuses the same array as a
	fmt.Println(a, b)     // both end in 20

	full := make([]int, 3) // no room to spare
	c := append(full, 10)
	d := append(full, 20) // has to allocate, so c is untouched
	fmt.Println(c, d)
}
[0 0 0 20] [0 0 0 20]
[0 0 0 10] [0 0 0 20]

a and b were appended from the same base slice. Because base had spare capacity, both appends wrote to the same array position, and b’s write clobbered a’s. In the second pair there was no spare capacity, so each append allocated and the results are independent. The behavior of append therefore depends on a capacity you may not have thought about, and the same code is correct or wrong depending on how the slice was made.

The rule that keeps you out of it: treat the slice you passed to append as invalid afterwards. Write s = append(s, x) and never keep the old value around. When you genuinely need to append to a shared base twice, cut the capacity with the three-index form, base[:3:3], which forces the allocation.

The slices package has the operations you would otherwise write by hand: slices.Sort, slices.Contains, slices.Index, slices.Equal, slices.Reverse, slices.Max, slices.SortFunc. Use them.

7.3 Maps

A map is a hash table from a comparable key type to any value type.

// Maps: creation, the comma-ok form, deletion, and ordered iteration.
package main

import (
	"fmt"
	"maps"
	"slices"
	"strings"
)

func main() {
	counts := map[string]int{}
	for _, w := range strings.Fields("the quick the lazy the end") {
		counts[w]++ // missing keys read as the zero value
	}

	n, ok := counts["the"]
	fmt.Println(n, ok)
	m, ok := counts["missing"]
	fmt.Println(m, ok)

	delete(counts, "lazy")
	fmt.Println(len(counts))

	// Iteration order is randomized, so sort the keys when order matters.
	for _, k := range slices.Sorted(maps.Keys(counts)) {
		fmt.Printf("%s=%d ", k, counts[k])
	}
	fmt.Println()

	var nilMap map[string]int
	fmt.Println(nilMap["anything"], len(nilMap)) // reads are fine
}
3 true
0 false
3
end=1 quick=1 the=3
0 0

counts[w]++ works on a key that is not there, because reading a missing key gives the zero value rather than an error. That makes counters and accumulators short, and it means an index expression cannot tell you whether the key exists. The comma-ok form does: v, ok := m[k] sets ok to whether the key was present. Use it whenever zero is a legitimate stored value.

delete(m, k) removes a key and does nothing if it was not there. len(m) counts the entries. clear(m) empties it.

Iteration order is randomized, deliberately and on every run, so that nobody writes code depending on an order the implementation does not promise. When you need a stable order, sort the keys, which slices.Sorted(maps.Keys(m)) does in one line.

The nil map is the one type whose zero value is not fully usable. Reading from it and taking its length work, and writing to it panics:

// Reading a nil map is fine. Writing to one is not.
package main

import "fmt"

func main() {
	var m map[string]int
	fmt.Println(m["absent"], len(m)) // 0 0
	m["absent"] = 1                  // panics
}
0 0
panic: assignment to entry in nil map

So a map field in a struct needs a make(map[K]V) before first write, while a slice field does not. That asymmetry catches everyone once.

Map elements are not addressable — you cannot write &m[k] or m[k].field = v — because a rehash can move them. When the value is a struct you want to modify, either store a pointer, map[string]*Thing, or read the struct out, change it, and put it back.

7.4 Strings

A string is an immutable sequence of bytes. It is conventionally UTF-8 but the language does not enforce that; a string can hold any bytes at all.

// A string is bytes; ranging over it yields runes.
package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	s := "héllo"

	fmt.Println(len(s), utf8.RuneCountInString(s))
	fmt.Println(s[1]) // a byte, not a character

	for i, r := range s {
		fmt.Printf("%d:%q ", i, r)
	}
	fmt.Println()

	r := []rune(s)
	fmt.Println(len(r), string(r[1]), string(r[4:]))
	fmt.Println([]byte(s)[:3])
}
6 5
195
0:'h' 1:'é' 3:'l' 4:'l' 5:'o'
5 é o
[104 195 169]

len(s) is a count of bytes, and s[i] is the i-th byte, of type byte. "héllo" is six bytes and five characters because é takes two, so len says 6 and s[1] is 195, the first of those two bytes rather than a character.

Ranging over a string decodes UTF-8 as it goes and gives you the byte index and the rune at it. That is why the indices above are 0, 1, 3, 4, 5 and skip 2. A malformed byte yields utf8.RuneError and advances by one, so the loop always terminates.

Three conversions, all of which allocate and copy. []byte(s) gives the bytes. []rune(s) decodes into a slice of code points, for when you need to index or slice by character. string(bs) and string(rs) go back. utf8.RuneCountInString(s) counts characters without allocating.

Strings are immutable, so s[0] = 'H' does not compile, and s += t in a loop allocates a new string every time. For anything more than a few concatenations use strings.Builder:

// Building a string without quadratic copying.
package main

import (
	"fmt"
	"strings"
)

func main() {
	var b strings.Builder
	for i := range 5 {
		fmt.Fprintf(&b, "%d,", i)
	}
	fmt.Println(strings.TrimSuffix(b.String(), ","))

	fmt.Println(strings.Join([]string{"a", "b", "c"}, "-"))
	fmt.Println(strings.Repeat("ab", 3))
	fmt.Println(strings.ToUpper("go"), strings.Contains("seafood", "foo"))
	fmt.Println(strings.Split("a,b,,c", ","))
	fmt.Printf("%q\n", strings.Fields("  spaced   out  "))
}
0,1,2,3,4
a-b-c
ababab
GO true
[a b  c]
["spaced" "out"]

A strings.Builder is an io.Writer, so fmt.Fprintf writes to it, the neatest way to assemble formatted output. Its zero value is ready to use.

The strings package is worth ten minutes with go doc strings. Note the two splitters that differ: strings.Split("a,b,,c", ",") keeps the empty field and returns four elements, while strings.Fields splits on runs of whitespace and returns none. strings.Cut is the modern way to split on the first separator.

7.5 Iterators

Since Go 1.23, range accepts a function of the right shape. That is how the standard library hands back sequences without building a slice. You saw one above: maps.Keys(m) yields keys, and slices.Sorted collects a sequence into a sorted slice. slices.Values, slices.All, and maps.All are the others you will meet first.

Writing your own means returning a func(yield func(T) bool), and you can go a long way without ever doing it. Consume them now; write them when a type of yours has a natural sequence and building a slice for it would be wasteful.

Exercise 7-1. Write func unique(xs []int) []int preserving first-seen order. Do it with a map for membership and a slice for order.

Exercise 7-2. Write a word-frequency program: read standard input, count words case-insensitively, and print the ten most common with their counts. slices.SortFunc will do the ranking.

Exercise 7-3. Write func reverse(s string) string that works on "héllo". Then check what it does to a string containing an emoji, and find out what a combining character is.

Exercise 7-4. Predict the output of append on a slice made with make([]int, 0, 4) versus one made with []int{}, then run it.