Foundations View as Markdown

Slices and maps

The two data structures you will use constantly, and the aliasing behaviour that surprises everyone once.

Slices#

A slice is a view onto an array: a pointer, a length and a capacity.

go
nums := []int{1, 2, 3}
nums = append(nums, 4)            // append returns a new slice header — reassign it
fmt.Println(len(nums), cap(nums)) // 4 4

empty := make([]int, 0, 100)      // length 0, capacity 100 — no reallocation for 100 appends

Preallocating capacity when you know the size is the cheapest performance win in Go.

go
out := make([]string, 0, len(users))
for _, u := range users {
    out = append(out, u.Name)
}

Slicing#

go
s := []int{0, 1, 2, 3, 4}
s[1:3]    // [1 2]
s[:2]     // [0 1]
s[2:]     // [2 3 4]

The slices package#

go
import "slices"

slices.Contains(nums, 3)
slices.Index(nums, 3)
slices.Sort(nums)
slices.SortFunc(users, func(a, b User) int { return cmp.Compare(a.Age, b.Age) })
slices.Reverse(nums)
slices.Clone(nums)
slices.Max(nums)

These are standard library. Generated Go still frequently hand-rolls contains and min, which is a good signal that the code came from older training data.

Maps#

go
ages := map[string]int{"ada": 36, "alan": 41}
ages["grace"] = 45
delete(ages, "alan")

age, ok := ages["ada"]      // the comma-ok idiom
if !ok {
    // not present — distinguishes "missing" from "present but zero"
}

The comma-ok form matters: ages["nobody"] returns 0 with no error, which is indistinguishable from a real zero without it.

go
counts := make(map[string]int)
counts["x"]++                // works — missing keys read as the zero value

Iteration order is random#

Deliberately. Go randomises it so you cannot depend on it.

go
keys := slices.Sorted(maps.Keys(m))    // Go 1.23+
for _, k := range keys {
    fmt.Println(k, m[k])
}

Any code that produces output by ranging a map directly will produce different results between runs. This is a common source of flaky tests in generated Go.

Maps are not safe for concurrent use#

Concurrent read and write panics at runtime with "concurrent map writes". Use a sync.Mutex, or sync.Map for the specific case of many readers and few writers.

Exercise#

go
package main

import "fmt"

func main() {
	words := []string{"go", "is", "fun", "go", "is", "fast", "go"}
	// Count how many times each word appears, then print the counts
	// for the words "go", "is", "fun" and "fast" — in that order.
	fmt.Println()
}

Common questions#

Slice or array?#

Slices, almost always. Arrays have a fixed size that is part of their type ([5]int and [6]int are different types) and they are copied by value on assignment. Arrays are for the rare case where the size is genuinely fixed and known at compile time.

Why does append sometimes modify the original slice?#

Because slices share a backing array. If there is spare capacity, append writes into it rather than allocating — and anything else viewing that array sees the change. slices.Clone when you want independence.

How do I get sorted map iteration?#

Collect the keys, sort them, then range the sorted keys. slices.Sorted(maps.Keys(m)) does it in one line on Go 1.23 and later.

Get the Go agent pack

A battle-tested AGENTS.md, the review checklist, and the failure-mode cheat sheet for Go. One email, then occasional updates when the tooling shifts. No course pitch.

Unsubscribe in one click. We never sell the list. Or just take the AGENTS.md now — no email needed.