Data Structures Deep Dive

Slice, map и string — структуры, которые Go-разработчик использует сотни раз в день. Но сколько из них могут объяснить, почему append иногда мутирует оригинал, а иногда нет? Почему нельзя взять &m[key]? Или почему len("Привет") возвращает 12, а не 6?
Профайлер регулярно показывает неожиданные аллокации на безобидном append, slice aliasing bugs проскальзывают в production, а разница между подходами к конкатенации строк может составлять 10x по производительности. Эти "базовые" структуры — одна из главных слепых зон Senior разработчиков.
Обзор структур данных
TL;DR: Memory Layout
| Структура | Header | Backing Storage | Pass-by | Mutable | Copy Trigger |
|---|---|---|---|---|---|
| slice | 24 bytes | Heap (backing array) | Value (header) | Elements: Yes | append beyond cap |
| string | 16 bytes | Heap or rodata | Value | No (immutable) | Any modification |
| map | 8 bytes | Heap (hmap+buckets) | Pointer | Yes | Never (reference) |
| array | N × sizeof(T) | Stack or Heap | Value (full copy) | Yes | Always on assign |
Ключевой инсайт
Slice и string — value types с pointer внутри: при передаче в функцию header копируется, но backing storage остаётся общим. Map — это pointer type: переменная m уже содержит указатель на runtime.hmap.
Memory Layout Overview
┌─────────────────────────────────────────────────────────────────────────────────┐
│ Data Structures Memory Layout │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ SLICE (24 bytes header) STRING (16 bytes header) │
│ ═══════════════════════ ════════════════════════ │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ ptr ─────────┐ │ │ ptr ─────────┐ │ │
│ │ len = 3 │ │ │ len = 12 │ │ (bytes, not runes) │
│ │ cap = 8 │ │ └──────────────┼──┘ │
│ └──────────────┼──┘ │ │
│ │ ▼ │
│ ▼ ┌─────────────────────┐ │
│ ┌──────────────────────┐ │ "Привет" (UTF-8) │ (immutable) │
│ │ [0] [1] [2] ... [7] │ (mutable) └─────────────────────┘ │
│ └──────────────────────┘ │
│ Heap Heap / rodata │
│ │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ MAP (8 bytes pointer) │
│ ═════════════════════ │
│ │
│ ┌─────────┐ ┌─────────────────────────────────────────────────────┐ │
│ │ *hmap ──┼─────▶│ hmap │ │
│ └─────────┘ │ ┌────────────────────────────────────────────────┐ │ │
│ │ │ count, B, hash0, buckets, oldbuckets, nevacuate│ │ │
│ │ └────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ buckets: [bucket0][bucket1]...[bucket_2^B] │ │ │
│ │ │ ├─tophash[8]─┤ │ │ │
│ │ │ ├─keys[8]────┤ │ │ │
│ │ │ ├─values[8]──┤ │ │ │
│ │ │ └─overflow───┘ │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ Heap │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘Структура раздела
Slice Internals
24-байтовый header (ptr, len, cap), backing array на heap, growth strategy (изменённая в Go 1.18). Escape analysis для размещения: когда slice целиком на stack. Внутренности runtime.growslice, расчёт новой capacity.
Slice Append
Семантика append: когда создаётся новый backing array, а когда мутируется существующий. Главная ловушка — shared array при cap > len. Решения: slices.Clone(), three-index slice s[low:high:max], явное копирование.
Map Internals
Структура hmap, bmap (bucket), overflow chains, evacuation при load factor ~6.5. Почему нельзя взять &m[key] — нет стабильных адресов. Iteration randomization как защита от hash-DoS. Специализированные map_fast*.go для частых типов ключей.
String & Rune
Immutable UTF-8 bytes под капотом. Индексация s[i] возвращает byte, не rune. range автоматически декодирует UTF-8. strings.Builder для эффективной конкатенации. Конвертации []byte ↔ string и когда они zero-copy.
Связь с Runtime
Операции над структурами данных тесно связаны с memory allocator:
make([]T, n)→runtime.makeslice→mallocgcmake(map[K]V, hint)→runtime.makemap→ bucket allocation- String literals →
rodatasection (не heap)
См. Stack vs Heap для понимания escape analysis и когда backing array размещается на stack.
Дальнейшее чтение
- Go Slices: usage and internals — официальный блог
- Go maps in action — официальный блог
- Strings, bytes, runes and characters — официальный блог
- Исходники:
$GOROOT/src/runtime/slice.go,map.go,string.go - slices package — Go 1.21+
- maps package — Go 1.21+