Go Data Structures
Pick the structure that fits the access pattern — not the most familiar one. Slices and maps are the workhorses; arrays, container types, and the slices/maps packages cover the rest. Understanding the header layout, growth costs, and copy semantics of each turns most performance questions into one-line decisions.
Core Rules
- Slices and maps are reference types — assigning copies the header, not the data. Use
slices.Clone/maps.Clonefor a true copy. - Preallocate with
make([]T, 0, n)andmake(map[K]V, n)whenever the size is known or estimable. - Always assign the result of
append— the backing array may move. - Use
slicesandmapspackages (Go 1.21+) instead of hand-rolled helpers. map[K]struct{}is the canonical set — zero-byte values, no boolean ambiguity.strings.Builderfor string building,bytes.Bufferwhen you needio.Reader/io.Writer.
Picking a Structure
What do you need?
├─ Ordered, fixed compile-time size → [N]T array
├─ Ordered, dynamic size → []T slice
│ ├─ Known size → make([]T, 0, n)
│ └─ JSON output must be [] → []T{} literal (not nil)
├─ Key/value lookup → map[K]V
│ ├─ Need a set → map[K]struct{}
│ └─ Known size → make(map[K]V, n)
├─ Priority queue / top-k → container/heap
├─ Frequent middle insertion → container/list
├─ Fixed-size rolling window → container/ring
├─ Pure string building → strings.Builder
└─ Read+write of bytes → bytes.Buffer
Slice Internals
A slice is a 3-word header: pointer, length, capacity. Multiple slices can alias the same backing array — s[1:4] shares memory with s.
Capacity Growth
The exact algorithm has changed across versions; do not rely on it. As of recent Go:
len < 256→ capacity roughly doubles.len ≥ 256→ grows by ~25%.- Each growth allocates a new backing array and copies — O(n) per growth.
Preallocation
users := make([]User, 0, len(ids)) // exact size
results := make([]Result, 0, estimated) // approximate
s = slices.Grow(s, additional) // pre-grow before bulk append (Go 1.21+)
slices Package (Go 1.21+)
| Function | Purpose |
|---|---|
Sort, SortFunc, SortStableFunc |
sorting |
BinarySearch, BinarySearchFunc |
sorted lookup |
Contains, Index, IndexFunc |
search |
Compact, CompactFunc |
dedupe adjacent equals |
Clone, Equal |
safe copy / comparison |
Delete, DeleteFunc |
removal preserving order |
Grow |
preallocate before append |
Concat (1.22+) |
concatenate slices |
Prefer these over hand-rolled loops — they're tested, generic, and use the fastest available paths.
Read references/slices-and-maps.md for capacity growth, aliasing pitfalls, and 2-D slice patterns.
nil vs Empty Slice: The JSON Trap
Both have len == 0 and cap == 0, but they encode differently:
var nilSlice []string // → JSON: null
emptySlice := []string{} // → JSON: []
API contracts almost always want []. Initialise the slice explicitly in any struct that gets marshaled to JSON, and treat nil/empty as identical when reading (use len(s) == 0).
For internal computation where nil is never marshaled, the nil slice is conventional and slightly cheaper (no allocation until first append).
Maps
Maps are hash tables with 8-entry buckets and overflow chains. They are reference types — assigning a map copies a pointer.
Preallocation
m := make(map[string]*User, len(users)) // avoids rehashing during population
The size hint is approximate (it's about bucket count), but it still saves repeated rehashing in the common case.
Sets
type Set[T comparable] map[T]struct{}
func (s Set[T]) Add(v T) { s[v] = struct{}{} }
func (s Set[T]) Has(v T) bool { _, ok := s[v]; return ok }
func (s Set[T]) Remove(v T) { delete(s, v) }
struct{} is zero bytes; the set is just the key set of the underlying map.
map[K]bool is also common but ambiguous: did false mean "explicitly excluded" or "not present"? struct{} removes the question.
maps Package (Go 1.21+)
Clone, Equal/EqualFunc, DeleteFunc; Keys, Values, Collect, Insert since 1.23 (iterators).
Read references/strings-bytes-builder.md for string-vs-bytes,
BuildervsBuffer, and rune handling.
Arrays
Fixed-size, value type, copied on assignment. Useful for compile-time-known sizes:
type Digest [32]byte
type IP4 [4]byte
cache := map[[2]int]Result{} // arrays are comparable → usable as map keys
For anything dynamic, use a slice.
container/* and Third-Party
| Package | Use case | Caveat |
|---|---|---|
container/heap |
priority queue, top-K | implement the interface yourself |
container/list |
LRU, frequent middle splice | poor cache locality |
container/ring |
rolling window, round-robin | fixed size |
bufio |
I/O with many small reads/writes | always check Flush errors |
For typed sets/queues/trees beyond the stdlib, prefer well-tested libraries (emirpasic/gods, gammazero/deque) and benchmark before optimising.
Read references/containers-and-pointers.md for heap implementation,
unsafe.Pointer's six valid patterns, andweak.Pointer[T].
Copy Semantics Cheat Sheet
| Type | Copy behaviour |
|---|---|
| primitives, arrays, structs | value (deep for contained value fields) |
| slice | header copied, backing array shared — use slices.Clone |
| map, channel | reference copied — use maps.Clone for maps |
*T, interface |
address / (type, value) pair copied |
Anti-Patterns
| Anti-pattern | Why it hurts | Do this instead |
|---|---|---|
s := append(s, x) ignoring return |
Backing array may move; s becomes stale |
Always reassign |
var m map[K]V; m[k] = v |
nil map panic | m := make(map[K]V) or map[K]V{} |
var s []T then marshal to JSON as [] |
Encodes as null |
s := []T{} |
make([]T, 0, 10000) "just in case" |
Wasted memory | Size by actual data |
m := map[K]bool{} as a set |
false is ambiguous |
map[K]struct{} |
bytes.Buffer for pure string building |
Extra copy in String() |
strings.Builder |
| Large struct values in a map | Each lookup copies the value | map[K]*V |
Verification Checklist
- Every
make([]T, ...)andmake(map[K]V, ...)has a capacity hint when the size is known. - Every
appendreassigns its result. - Slices marshaled to JSON are initialised as
[]T{}, notvar s []T. - All "sets" use
map[K]struct{}(or a genericSet[T]wrapper). - No
bytes.Bufferused purely forString()output. - No
*sync.Mutexcopied via struct assignment (go vet copylocks). -
slices.Clone/maps.Cloneused when handing data to callers that may mutate.
References
- references/slices-and-maps.md — internals, growth, aliasing,
slices/mapspackages - references/strings-bytes-builder.md —
strings.Builder,bytes.Buffer, rune handling - references/containers-and-pointers.md —
container/heap, generic wrappers,unsafe.Pointer,weak.Pointer