Slice and Map Internals

Updated

September 8, 2026

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 9

append 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 unchanged

Full slice expression

t := s[i:j:k] // len=j-i, cap=k-i

Limits 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

  1. Not addressable elements — cannot take &m[k] for map values in general.
  2. Iteration order is randomized — never depend on it.
  3. Concurrent map read+write without sync panics (fatal runtime error).
  4. Delete during range is allowed for the current key; still no concurrent writers from other Gs.
  5. 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)

Sharing and Aliasing Checklist

Action Shares backing array?
b := a Yes
b := a[i:j] Yes
append that grows No (new array for result)
slices.Clone / copy to new No
maps.Clone New map, same element values

When returning a slice from an internal buffer, clone if the buffer will be reused.

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.