How it works
The explanation, with examples you can run.
Slices share arrays
s[i:j] makes a new header that points into the same array. Writes through one slice are visible through every other slice of that array. append is where it gets subtle:
package main import "fmt" func main() { base := make([]int, 3, 4) // len 3, cap 4: one free slot a := append(base, 1) // fits: writes into base's array b := append(base, 2) // fits too: overwrites the same slot fmt.Println(a[3], b[3]) // 2 2 c := append(base[:3:3], 3) // full slice expression caps it at 3, so append copies fmt.Println(c[3], a[3]) // 3 2 var s []int last := -1 for i := 0; i < 2000; i++ { s = append(s, i) if cap(s) != last { fmt.Print(cap(s), " ") last = cap(s) } } fmt.Println()}When capacity runs out, append allocates a larger array, copies, and returns a header for the new one. The old slice still points at the old array. That is why you must always use the result: s = append(s, x). With Go 1.26 the capacities printed were 4 8 16 32 64 128 256 512 848 1280 1792 2560: doubling while small, then growing by a smaller factor, rounded up to the allocator's size classes. The exact numbers are an implementation detail and change between releases; only "amortised constant time" is promised.
Two practical consequences:
- A function that appends to a slice it was given may, or may not, change the caller's elements. Return the new slice and document who owns it.
- A small slice of a huge array keeps the whole array alive. Copy out what you need (
slices.Clone) before storing it.
Maps
A map value is a pointer to a runtime hash table, so a nil map can be read (it is empty) but writing to it panics. Since Go 1.24 the table is based on Swiss tables, which made lookups and inserts faster. It didn't change the rules:
- Iteration order is unspecified. The spec doesn't promise any order, and in practice the order differs from one
rangeloop to the next. Running a loop over a five-key map three times printedeabcd abcde cdeab. For a stable order, sort the keys:slices.Sorted(maps.Keys(m)). - Entries aren't addressable.
&m[k]doesn't compile, because growing the table moves entries. Store pointers, or read, modify and write back. - No concurrent writes. If one goroutine writes while another reads or writes, that is a data race; the runtime often notices and stops with
fatal error: concurrent map writes. Use async.Mutex, orsync.Mapfor its two specialised cases (keys written once and read many times, or goroutines working on disjoint keys).
Since Go 1.22, for i := range n counts from 0 to n-1, and each loop iteration gets its own copy of the loop variables, so closures and goroutines started inside a loop no longer all see the last value.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.