Slices and maps are Go's primary collection types and understanding their internals is essential for writing correct, performant code. A slice is a three-word struct: a pointer to a backing array, a length, and a capacity. Appending beyond capacity allocates a new, larger array and copies the data. This means slices are reference-like (two slices can share a backing array) but assignments and function arguments copy the header, not the data — so append inside a function won't be visible to the caller unless you return the new slice.
Maps are hash tables with randomized iteration order. They're reference types — passing a map to a function lets that function modify the original. The zero value of a map is nil, and reading from a nil map is safe (returns the zero value), but writing to one panics. The comma-ok idiom (val, ok := m[key]) distinguishes between a missing key and a key whose value is the zero value.
Go 1.21 introduced the slices and maps packages in the standard library, providing generic functions for sorting, searching, comparing, cloning, and transforming slices and maps. These replace a large number of hand-written loops and the older sort.Slice patterns. Go 1.22 added range over int and Go 1.23 added iterator functions — both of which work naturally with slices and maps.
Master go slices and maps
Take the Go Programming course with hands-on lessons and challenges.
This demonstrates how slices share backing arrays. The full slice expression s[lo:hi:max] limits capacity, forcing append to allocate a new array. Use it when you need to hand off a sub-slice without risking accidental mutation of the original data.
Pre-allocating with make([]T, 0, n) avoids repeated allocations and copies. When the exact count is known, use make([]T, n) and index directly. This is one of the most common Go performance optimizations and is flagged by linters like prealloc.
The slices package replaces most hand-written slice manipulation loops. Sort, Compact, Contains, BinarySearch, Delete, and Clone cover the majority of real-world needs. Always use slices.Clone when you need an independent copy to avoid shared backing array issues.
Maps are hash tables with randomized iteration order. The comma-ok idiom is essential for distinguishing absent keys from zero values. The maps package (Go 1.21+) provides Clone, Equal, Keys, and Values. For sorted iteration, use slices.Sorted(maps.Keys(m)).
Nil and empty slices are functionally identical for len, cap, range, and append. The critical difference is JSON serialization: nil becomes null, empty becomes []. API responses almost always need [], so initialize with make([]T, 0) or a literal. Nil maps are safe to read but panic on write.
Generic helper functions for common slice operations. filter uses slices.Clone + DeleteFunc to avoid mutating the input. deduplicate uses a map[E]struct{} set for O(1) lookups. chunk uses progressive slicing. The min builtin (Go 1.21+) replaces the manual min helper.
Appending to a slice inside a function and expecting the caller to see the new elements without returning the slice
append may allocate a new backing array, which means the caller's slice header (pointer, len, cap) is stale. Always return the result of append: `s = append(s, v)`. If a function grows a slice, it must return the new slice or accept a pointer to the slice header.
Iterating over a map and assuming a stable order — then being surprised when tests fail intermittently
Go randomizes map iteration order deliberately. If you need deterministic order, collect the keys with maps.Keys(), sort them with slices.Sort(), and iterate over the sorted keys. Never rely on insertion order.
Writing to a nil map, which causes a runtime panic
The zero value of a map is nil. Reading is safe (returns the zero value), but writing panics. Always initialize maps with make(map[K]V) or a map literal before writing. A common pattern is lazy initialization: if m == nil { m = make(map[K]V) }.
Creating sub-slices that unintentionally keep the entire backing array alive, preventing garbage collection of a large underlying array
If you only need a small portion of a large slice, use slices.Clone() or copy() to create an independent slice. This lets the garbage collector reclaim the large backing array. This matters when reading large files and keeping a small subset of rows.
Slices are three-word headers (pointer, length, capacity) backed by arrays, and understanding this explains append behavior, shared mutations, and capacity management. Maps are reference-type hash tables with randomized iteration order, where the comma-ok idiom is essential for safe key lookups. Go 1.21+ introduced the slices and maps packages, providing generic functions that replace most hand-written collection manipulation code. Pre-allocate when sizes are known, clone when independence is needed, and always return the result of append.
Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.
Interactive lessons and challenges, right in your code editor.
Check the free courses. No credit card.