Slice and Map Internals
Slice and Map Internals
Overview
Slices and maps are the two most used runtime-backed structures. Misunderstanding headers and growth causes accidental sharing bugs, huge realloc copies, and production map panics.
Basics: Arrays & slices, Maps.
Diagram: Slice header and map grow
regions: slice header | map
flow:
[ptr]
|
v
[Arr]
[buckets] --load high--> [grow]
Slice Header
A slice value is a small header (conceptually):
slice
ptr -> underlying array
len
cap
Assignment copies the header, not the array:
a := []int{1, 2, 3}
b := a
b[0] = 9 // a[0] is also 9append may allocate a new array when len+need > cap, then copy. Capacity growth is geometric (implementation detail; treat as amortized O(1) append).
s = append(s, x) // maybe new backing store; old aliases unchangedFull slice expression
t := s[i:j:k] // len=j-i, cap=k-iLimits capacity so later append cannot overwrite the rest of the original array — critical when returning subslices from internal buffers.
Nil vs empty
var s []int // nil, len0 cap0
t := []int{} // non-nil empty
u := make([]int, 0)JSON encodes both as []. Prefer one convention in APIs.
Map Runtime Model
Maps are hash tables of buckets (plus overflow). Runtime may:
- Grow (reallocate buckets) when load is high
- Evacuate old buckets progressively
map header (hmap)
count
B // log2 buckets
buckets
oldbuckets // during grow
...
Semantics that bite
- Not addressable elements — cannot take
&m[k]for map values in general. - Iteration order is randomized — never depend on it.
- Concurrent map read+write without sync panics (fatal runtime error).
- Delete during range is allowed for the current key; still no concurrent writers from other Gs.
- NaN float keys are cursed — avoid float map keys.
Growth cost
Large maps that grow under a request path create latency spikes. Pre-size when you know cardinality:
m := make(map[string]int, expected)Experiment
go mod init example
go run .package main
import (
"fmt"
"unsafe"
)
func main() {
// header size
var s []int
fmt.Println("sizeof slice header", unsafe.Sizeof(s))
a := make([]int, 3, 6)
a[0], a[1], a[2] = 1, 2, 3
b := a[1:3]
b[0] = 99
fmt.Println("share", a) // a[1] changed
// cap limit
c := a[0:2:2]
c = append(c, 7)
fmt.Println("full slice a after append c", a, "c", c)
// growth
var d []int
for i := 0; i < 10; i++ {
d = append(d, i)
fmt.Printf("len=%d cap=%d\n", len(d), cap(d))
}
// map pre-size vs not (just API demo)
m := make(map[int]int, 100)
for i := 0; i < 100; i++ {
m[i] = i * i
}
fmt.Println("map len", len(m))
// iteration order differs across runs (print a few)
keys := 0
for k := range m {
keys++
if keys <= 3 {
fmt.Println("key sample", k)
}
}
}What to notice: Subslice mutation mutates the original; capacity growth jumps; map key order is not sorted.
Try next: Force concurrent map write with two goroutines (expect fatal); fix with sync.Mutex or sync.Map only if appropriate.