Maps в Go — под капотом
Maps — хеш-таблицы в Go. Часто кажется, что вы их понимаете, пока не доходит до вопросов про buckets, evacuation, итерацию, concurrent access и
sync.Map. Этот файл закрывает все типичные собеседовательные мины.
Содержание (TOC)
Заголовок раздела «Содержание (TOC)»- Базовое определение и API
- Внутреннее устройство (под капотом)
- Тонкие моменты / Gotchas
- Производительность
- Типичные вопросы на собеседовании Junior
- Practice — задачки на проверку
- Источники и дополнительно
1. Базовое определение и API
Заголовок раздела «1. Базовое определение и API»1.1 Что такое map
Заголовок раздела «1.1 Что такое map»Map — это hash table, ассоциативный массив “ключ → значение”. В Go это встроенный тип. Не упорядочен, поиск O(1) в среднем.
var m map[string]int // nil mapm1 := make(map[string]int)m2 := make(map[string]int, 100) // hint capacity (preallocation)m3 := map[string]int{"a": 1, "b": 2}1.2 Базовые операции
Заголовок раздела «1.2 Базовые операции»m := map[string]int{"a": 1, "b": 2}
// Записьm["c"] = 3
// Чтениеv := m["a"] // 1v2 := m["zzz"] // 0 (zero value!)
// ok-idiom — проверка существованияv3, ok := m["a"]fmt.Println(v3, ok) // 1 true
// Удалениеdelete(m, "a")
// Длинаfmt.Println(len(m))
// Итерация (порядок РАНДОМНЫЙ!)for k, v := range m { fmt.Println(k, v)}1.3 Допустимые типы ключей
Заголовок раздела «1.3 Допустимые типы ключей»Ключом может быть только comparable тип:
| Тип | Можно как ключ? |
|---|---|
int, float, string, bool | Да |
array | Да (если элементы comparable) |
struct | Да (если все поля comparable) |
pointer | Да (сравнивает адреса) |
interface | Да (с runtime panic, если динамический тип не comparable) |
chan | Да (сравнивает по identity) |
| slice | Нет |
| map | Нет |
| func | Нет |
m1 := map[[3]int]string{} // OKm2 := map[*int]int{} // OK// m3 := map[[]int]string{} // ОШИБКА// m4 := map[func()]int{} // ОШИБКА2. Внутреннее устройство (ПОД КАПОТОМ)
Заголовок раздела «2. Внутреннее устройство (ПОД КАПОТОМ)»2.1 Структура hmap
Заголовок раздела «2.1 Структура hmap»Тип map[K]V в Go — это указатель на структуру hmap.
// runtime/map.go (упрощённо)type hmap struct { count int // # количество элементов flags uint8 // флаги (iteration, write и т.д.) B uint8 // log2 числа buckets: 2^B buckets noverflow uint16 // приблизит. число overflow buckets hash0 uint32 // seed для хеш-функции buckets unsafe.Pointer // массив бакетов, 2^B штук oldbuckets unsafe.Pointer // во время роста: старый массив бакетов nevacuate uintptr // прогресс эвакуации при росте extra *mapextra // overflow buckets и т.д.}2.2 Bucket (bmap)
Заголовок раздела «2.2 Bucket (bmap)»Каждый bucket хранит до 8 пар ключ-значение. После 8 элементов — overflow bucket.
// runtime/map.go (упрощённо)type bmap struct { tophash [8]uint8 // верхние 8 бит хеша каждого элемента // далее в памяти (генерируется компилятором): // keys [8]K // values [8]V // overflow *bmap}2.3 ASCII-схема bucket layout
Заголовок раздела «2.3 ASCII-схема bucket layout»hmap (header)┌──────────────────────────────┐│ count = 12 ││ B = 2 (значит 4 buckets) ││ hash0 = 0xDEADBEEF ││ buckets ──────┐ │└──────────────┘│ │ ▼buckets array (2^B = 4 элементов):┌─────────┬─────────┬─────────┬─────────┐│ Bucket0 │ Bucket1 │ Bucket2 │ Bucket3 │└────┬────┴─────────┴─────────┴─────────┘ │ ▼Bucket0 (bmap):┌────────────────────────────────────────────┐│ tophash[0..7]: AB CD EF 12 34 .. .. .. │ ← 8 байт, верхние 8 бит хеша│ keys[0..7]: k0 k1 k2 k3 k4 .. .. .. │ ← все ключи рядом│ values[0..7]: v0 v1 v2 v3 v4 .. .. .. │ ← все значения рядом│ overflow: ──┐ │└──────────────────┼────────────────────────┘ ▼ Overflow bucket (если bucket переполнился)⚠️ Важно: keys и values хранятся отдельными массивами (не как [K,V] пары). Это улучшает кэш-локальность при поиске по ключу.
2.4 Hash function
Заголовок раздела «2.4 Hash function»Go использует memhash — низкоуровневую хеш-функцию для большинства встроенных типов. Для строк — оптимизирован через AES-инструкции на современных CPU.
// runtime/alg.go (упрощённо)func memhash(p unsafe.Pointer, seed, s uintptr) uintptrfunc strhash(a unsafe.Pointer, h uintptr) uintptrHash рандомизирован при инициализации программы через hash0 (seed). Это нужно для:
- Защиты от hash-collision DoS атак.
- Рандомизации итерации (см. ниже).
2.5 Алгоритм поиска (get)
Заголовок раздела «2.5 Алгоритм поиска (get)»1. Вычисляем хеш ключа: h := hash(k, hmap.hash0)2. Выделяем lower B бит h → индекс bucket: bIdx = h & (2^B - 1)3. Идём в bucket по индексу. Берём tophash = (h >> 56) & 0xFF (верхние 8 бит)4. Сравниваем tophash[i] с искомым для каждого i из 0..75. Если tophash совпал — сравниваем keys[i] с искомым ключом6. Если ключ нашёлся — возвращаем values[i]7. Если не нашли — идём в overflow bucket (если есть)8. Если до конца не нашли — возвращаем zero value2.6 Growth (рост) — incremental rehash
Заголовок раздела «2.6 Growth (рост) — incremental rehash»Когда map становится плотным, происходит рост (увеличение числа buckets).
Триггер:
count > 6.5 * 2^B(load factor > 6.5)- ИЛИ слишком много overflow buckets
Алгоритм роста:
- Аллоцируется новый массив buckets вдвое больше (B+1 → 2^(B+1) buckets).
- Старый массив НЕ копируется сразу. Это было бы дорого.
- Каждая операция
set/deleteпереносит 1-2 старых bucket в новый. getсмотрит сначала в новый, потом в старый, если ещё не эвакуирован.
Это называется incremental rehash — амортизированная стоимость операций.
2.7 Evacuation — куда переезжают ключи
Заголовок раздела «2.7 Evacuation — куда переезжают ключи»При росте каждый ключ из bucket i (старого массива) может попасть в bucket i или i + 2^B (нового массива) — в зависимости от того, был ли в его хеше включён очередной “новый бит”.
B=2 → 4 buckets (индексы 0..3)B=3 → 8 buckets (индексы 0..7)Старый bucket 0 → новые buckets 0 ИЛИ 4Старый bucket 1 → новые buckets 1 ИЛИ 52.8 Iteration: почему рандомная
Заголовок раздела «2.8 Iteration: почему рандомная»Go специально рандомизирует порядок итерации:
- Начальный bucket выбирается случайно.
- Стартовая позиция внутри bucket — тоже случайна.
m := map[string]int{"a":1, "b":2, "c":3}for k := range m { fmt.Println(k) // разный порядок каждый запуск}Зачем: чтобы разработчики не закладывались на порядок (как, например, в Python <3.7 с insertion order). Если бы Go давал стабильный порядок, при росте hash table порядок мог бы внезапно измениться, и старый код сломался бы.
✅ Если нужен стабильный порядок — отсортируйте ключи отдельно:
keys := make([]string, 0, len(m))for k := range m { keys = append(keys, k) }sort.Strings(keys)for _, k := range keys { fmt.Println(k, m[k]) }3. Тонкие моменты / Gotchas
Заголовок раздела «3. Тонкие моменты / Gotchas»Gotcha 1: nil map — write panic, read OK
Заголовок раздела «Gotcha 1: nil map — write panic, read OK»var m map[string]int // nilv := m["a"] // OK, возвращает 0v, ok := m["a"] // OK, v=0, ok=falselen(m) // OK, 0for range m {} // OK, 0 итераций
m["a"] = 1 // PANIC: assignment to entry in nil mapdelete(m, "a") // OK (no-op в Go 1.x)✅ Всегда инициализируйте: m := make(map[string]int) или m := map[string]int{}.
Gotcha 2: Zero value для отсутствующего ключа
Заголовок раздела «Gotcha 2: Zero value для отсутствующего ключа»m := map[string]int{}v := m["nonexistent"] // 0 (zero value!)Из-за этого нельзя отличить “ключ не существует” от “ключ есть, значение 0”. Используйте ok-idiom:
v, ok := m["key"]if !ok { /* ключа нет */ }Gotcha 3: Iteration order рандомный
Заголовок раздела «Gotcha 3: Iteration order рандомный»m := map[string]int{"a":1, "b":2, "c":3}for k := range m { fmt.Print(k, " ") // каждый запуск разный порядок}⚠️ Не пишите тесты, ожидающие фиксированный порядок.
Gotcha 4: Concurrent access — race + panic
Заголовок раздела «Gotcha 4: Concurrent access — race + panic»m := map[string]int{}
// Goroutine 1:go func() { m["a"] = 1 }()
// Goroutine 2:go func() { _ = m["a"] }()Запуск с -race покажет data race. Без race detector — может быть panic: fatal error: concurrent map writes или concurrent map iteration and map write.
✅ Решения:
sync.Mutex/sync.RWMutexвокруг операций.sync.Map— для специфичных паттернов.
Gotcha 5: sync.Map — НЕ для всех случаев
Заголовок раздела «Gotcha 5: sync.Map — НЕ для всех случаев»sync.Map оптимизирован под:
- Read-heavy доступ.
- Disjoint sets: разные горутины пишут разные ключи.
Для других сценариев (write-heavy) — map + sync.RWMutex быстрее.
var sm sync.Mapsm.Store("k", 1)v, ok := sm.Load("k")sm.Delete("k")sm.Range(func(k, v any) bool { return true})⚠️ sync.Map использует any, теряете типобезопасность. С Go 1.18+ есть generics-обёртки.
Gotcha 6: Нельзя взять адрес значения в map
Заголовок раздела «Gotcha 6: Нельзя взять адрес значения в map»m := map[string]int{"a": 1}// p := &m["a"] // ОШИБКА компиляцииПричина: при росте map значение может переехать в другую память. Адрес стал бы невалидным.
✅ Workaround:
m := map[string]*int{"a": new(int)}*m["a"] = 5 // OKp := m["a"] // OK, это копия указателя, но указывает на ту же памятьGotcha 7: Изменение struct-значения в map
Заголовок раздела «Gotcha 7: Изменение struct-значения в map»type Pt struct{ X, Y int }m := map[string]Pt{"a": {1, 2}}// m["a"].X = 10 // ОШИБКА: cannot assign to struct field m["a"].XПричина: m["a"] — это копия значения. Изменение копии бессмысленно.
✅ Решения:
// 1. Получить, изменить, положить обратно:p := m["a"]p.X = 10m["a"] = p
// 2. Использовать pointer:m2 := map[string]*Pt{"a": {1, 2}}m2["a"].X = 10 // OKGotcha 8: delete не освобождает память
Заголовок раздела «Gotcha 8: delete не освобождает память»m := make(map[int]int)for i := 0; i < 1_000_000; i++ { m[i] = i}for i := 0; i < 1_000_000; i++ { delete(m, i)}// len(m) == 0, но размер buckets остался прежним!Map никогда не уменьшает число buckets. Память освобождается только при m = nil или новом make.
✅ Если нужно “сбросить”:
m = make(map[int]int)// или Go 1.21+:clear(m) // обнуляет все значения, но cap не меняется⚠️ clear(m) ускоряет очистку, но не уменьшает аллоцированную память.
Gotcha 9: Iteration safety — что можно/нельзя
Заголовок раздела «Gotcha 9: Iteration safety — что можно/нельзя»✅ Во время range можно:
- Удалять текущий ключ (
delete). - Удалять любой ключ.
- Менять значение существующего ключа.
⚠️ Во время range рискованно:
- Добавлять новые ключи — могут или не могут попасть в текущую итерацию (поведение не определено).
for k := range m { if k == "trigger" { m["new"] = 1 // увидим или нет — НЕ ОПРЕДЕЛЕНО }}✅ Если нужно гарантированно итерировать, копируйте ключи:
keys := make([]string, 0, len(m))for k := range m { keys = append(keys, k) }for _, k := range keys { // безопасно меняем m}Gotcha 10: NaN как ключ — недостижим
Заголовок раздела «Gotcha 10: NaN как ключ — недостижим»m := map[float64]string{}m[math.NaN()] = "x"v, ok := m[math.NaN()]fmt.Println(v, ok) // "" false (!!)Причина: NaN != NaN в IEEE 754. Map по-разному хеширует, и сравнение на этапе lookup всегда возвращает false.
Gotcha 11: int vs string ключи — разный hash performance
Заголовок раздела «Gotcha 11: int vs string ключи — разный hash performance»string ключи дороже хешировать (особенно длинные). Если возможно, используйте int/uint как ключи.
Gotcha 12: capacity hint в make
Заголовок раздела «Gotcha 12: capacity hint в make»m := make(map[string]int, 1000)Это подсказка компилятору сразу выделить достаточно buckets под ~1000 элементов. Это не лимит, а оптимизация — избегает многократного расширения.
Gotcha 13: Сравнение интерфейсов в map ключах
Заголовок раздела «Gotcha 13: Сравнение интерфейсов в map ключах»type Stringer interface{ String() string }m := map[Stringer]int{}m[nil] = 0 // OK
var s Stringer = []int{1,2,3} // ошибка: []int не реализует StringerЕсли динамический тип интерфейса не comparable — будет runtime panic при вставке.
Gotcha 14: Возврат map[K]V — это указатель
Заголовок раздела «Gotcha 14: Возврат map[K]V — это указатель»func makeMap() map[string]int { return map[string]int{"a": 1}}
m := makeMap()m["b"] = 2 // влияет на оригинал, т.к. map — указательMap передаются “по ссылке” (как slice header, но проще — это указатель на hmap).
Gotcha 15: Comparable интерфейсы (Go 1.20+)
Заголовок раздела «Gotcha 15: Comparable интерфейсы (Go 1.20+)»type Key interface { comparable }
m := map[any]int{} // OK с Go 1.20+m[5] = 1m[[]int{1,2,3}] = 2 // PANIC в runtime: hash of unhashable typeС Go 1.20 расширили comparable, но runtime-panic остался.
Gotcha 16: Длинные строковые ключи — risk
Заголовок раздела «Gotcha 16: Длинные строковые ключи — risk»Длинные строки в качестве ключей могут попасть в один и тот же overflow bucket, если хешируются одинаково. Это редко, но deterministic hash может быть атакован → DoS.
✅ Go использует рандомизированный seed (hash0) при старте программы — защита от hash flood DoS.
4. Производительность
Заголовок раздела «4. Производительность»4.1 Preallocation через size hint
Заголовок раздела «4.1 Preallocation через size hint»// Плохо:m := map[int]int{}for i := 0; i < 1_000_000; i++ { m[i] = i}
// Хорошо:m := make(map[int]int, 1_000_000)for i := 0; i < 1_000_000; i++ { m[i] = i}Выигрыш — избегаем многократных evacuation.
4.2 Структуры vs указатели как values
Заголовок раздела «4.2 Структуры vs указатели как values»type Big struct { /* много полей */ }
// Вариант A: structmA := map[string]Big{}// При m[k] = v — копируется вся структура
// Вариант B: pointermB := map[string]*Big{}// При m[k] = v — копируется только указательЕсли Big большой — указатели быстрее. Минус: добавляет уровень indirection (плохо для cache).
4.3 sync.Map vs RWMutex+map
Заголовок раздела «4.3 sync.Map vs RWMutex+map»// sync.Map — лучше при:// - 90%+ операций — Load// - Каждая горутина пишет в свой набор ключей
// map + sync.RWMutex — лучше при:// - Mixed read/write// - Малое число горутин// - Нужна типобезопасностьБенчмарки показывают: sync.Map может быть в 2-3x медленнее обычного map+Mutex при write-heavy.
4.4 String interning для ключей
Заголовок раздела «4.4 String interning для ключей»Если у вас миллионы записей с одинаковыми/похожими строками, используйте string interning — кэширование уникальных строк:
var intern = map[string]string{}func Intern(s string) string { if v, ok := intern[s]; ok { return v } intern[s] = s return s}Экономит память.
4.5 clear() — Go 1.21
Заголовок раздела «4.5 clear() — Go 1.21»// До 1.21:for k := range m { delete(m, k) }
// С 1.21:clear(m) // быстрее4.6 Map literal vs append
Заголовок раздела «4.6 Map literal vs append»// Литерал — одна аллокация:m := map[string]int{"a":1, "b":2, "c":3}
// vs build:m := make(map[string]int)m["a"] = 1m["b"] = 2m["c"] = 3Литерал немного быстрее для маленьких maps (компилятор оптимизирует).
5. Типичные вопросы на собеседовании Junior
Заголовок раздела «5. Типичные вопросы на собеседовании Junior»Q1: Что такое map в Go?
Заголовок раздела «Q1: Что такое map в Go?»A: Встроенная hash table, ассоциативный массив “ключ → значение”. Под капотом — указатель на hmap-структуру с массивом buckets (по 8 элементов в каждом). Поиск амортизированный O(1).
Q2: Можно ли использовать slice как ключ?
Заголовок раздела «Q2: Можно ли использовать slice как ключ?»A: Нет. Slice не comparable. Ключ должен быть comparable: int, string, struct (если поля comparable), array, pointer, interface (с runtime check).
Q3: Что вернёт m["nonexistent_key"]?
Заголовок раздела «Q3: Что вернёт m["nonexistent_key"]?»A: Zero value для типа значения. Для map[string]int — 0. Для map[string]string — "". Чтобы отличить — используйте ok-idiom: v, ok := m[k].
Q4: Можно ли писать в nil-map?
Заголовок раздела «Q4: Можно ли писать в nil-map?»A: Нет, panic: assignment to entry in nil map. Но читать из nil-map можно (вернёт zero value). len(nil-map) == 0, delete(nil-map, k) — no-op.
Q5: Что происходит при concurrent access?
Заголовок раздела «Q5: Что происходит при concurrent access?»A: Один читает + один пишет (или 2 пишут) → race condition. Go’s runtime детектит это и панирует: fatal error: concurrent map writes. Защита: sync.Mutex/RWMutex или sync.Map.
Q6: Чем sync.Map отличается от обычной map?
Заголовок раздела «Q6: Чем sync.Map отличается от обычной map?»A:
sync.Mapпотокобезопасен из коробки.- Использует
any(нет типобезопасности). - Оптимизирован под read-heavy и disjoint keys.
- Для write-heavy паттернов
map + sync.RWMutexбыстрее.
Q7: Можно ли взять адрес значения в map?
Заголовок раздела «Q7: Можно ли взять адрес значения в map?»A: Нет. &m[k] — ошибка компиляции. Причина: при росте map значение может переехать. Решение — хранить указатели: map[K]*V.
Q8: Почему iteration order рандомный?
Заголовок раздела «Q8: Почему iteration order рандомный?»A: Go специально рандомизирует, чтобы разработчики не закладывались на порядок. Это защита от bugs, когда поведение программы зависит от внутренней структуры map.
Q9: Можно ли менять значение struct в map напрямую?
Заголовок раздела «Q9: Можно ли менять значение struct в map напрямую?»A: Нет. m["a"].Field = 1 — ошибка. Нужно: получить копию, изменить, положить обратно (p := m["a"]; p.Field = 1; m["a"] = p). Или хранить указатели: map[K]*V.
Q10: Освобождается ли память при delete?
Заголовок раздела «Q10: Освобождается ли память при delete?»A: Нет. Map не уменьшает число buckets. Память освободится только при m = nil или новом make. С Go 1.21 есть clear(m) — обнуляет значения, но cap остаётся.
Q11: Что такое load factor в Go maps?
Заголовок раздела «Q11: Что такое load factor в Go maps?»A: Отношение количества элементов к числу buckets × 8. Если > 6.5 — триггерится рост (удвоение buckets, постепенная эвакуация). Это компромисс между скоростью и памятью.
Q12: Что такое overflow bucket?
Заголовок раздела «Q12: Что такое overflow bucket?»A: Bucket, к которому добавляется ещё один, когда основной заполнен (>8 элементов с тем же индексом). Создаёт linked list. При слишком многих overflow buckets — также триггерится рост.
Q13: Что выведет?
Заголовок раздела «Q13: Что выведет?»m := map[int]string{1:"a", 2:"b", 3:"c"}delete(m, 4)fmt.Println(len(m))A: 3. Удаление несуществующего ключа — no-op, паники нет.
Q14: Что выведет?
Заголовок раздела «Q14: Что выведет?»m := make(map[string]int)m["a"]++fmt.Println(m["a"])A: 1. m["a"] вернул 0 (zero value), ++ сделал 1, записал.
Q15: Можно ли использовать map ключом в другой map?
Заголовок раздела «Q15: Можно ли использовать map ключом в другой map?»A: Нет, map не comparable. map[map[int]int]int{} — ошибка.
Q16: Что такое hash flood DoS?
Заголовок раздела «Q16: Что такое hash flood DoS?»A: Атака, при которой клиент посылает много ключей, хешируемых в один bucket → деградация O(1) до O(n). Защита Go: рандомизированный hash0 при старте.
Q17: Как итерироваться по map в отсортированном порядке?
Заголовок раздела «Q17: Как итерироваться по map в отсортированном порядке?»A: Скопировать ключи в slice, отсортировать, итерировать по slice.
keys := make([]string, 0, len(m))for k := range m { keys = append(keys, k) }sort.Strings(keys)for _, k := range keys { fmt.Println(k, m[k]) }Q18: Размер map header?
Заголовок раздела «Q18: Размер map header?»A: Несколько десятков байт (зависит от Go version, обычно ~48-56). Сам hmap-struct содержит ~10 полей. Плюс buckets.
Q19: Что выведет?
Заголовок раздела «Q19: Что выведет?»var m map[string]intv, ok := m["a"]fmt.Println(v, ok)A: 0 false. nil-map читается нормально.
Q20: Безопасно ли удалять элементы во время range?
Заголовок раздела «Q20: Безопасно ли удалять элементы во время range?»A: Да, спецификация гарантирует. Можно удалять текущий или любой другой ключ. Добавление новых ключей — undefined behavior (может попасть в итерацию или нет).
Q21: Что такое incremental rehash?
Заголовок раздела «Q21: Что такое incremental rehash?»A: Стратегия роста map: старые buckets не копируются сразу, а переносятся постепенно во время операций. Это снижает worst-case latency операций. Особенность Go’s map.
Q22: Чему равен cap(m)?
Заголовок раздела «Q22: Чему равен cap(m)?»A: Trick question: для map функция cap() не определена. Только len(). Capacity не доступна из user space.
Q23: Что произойдёт с программой?
Заголовок раздела «Q23: Что произойдёт с программой?»m := map[string]int{}go func() { m["a"] = 1 }()go func() { _ = m["a"] }()time.Sleep(time.Second)A: С -race — race detector сообщит. Без race detector — может работать, может паниковать с fatal error: concurrent map read and map write.
Q24: Можно ли использовать struct как ключ?
Заголовок раздела «Q24: Можно ли использовать struct как ключ?»A: Да, если все поля struct comparable. struct из примитивов — OK. struct со slice внутри — нет.
type Key struct { Name string; ID int }m := map[Key]int{} // OKQ25: Что такое clear(m) в Go 1.21?
Заголовок раздела «Q25: Что такое clear(m) в Go 1.21?»A: Встроенная функция, очищающая map (удаляет все ключи). Эквивалент: цикл с delete, но быстрее. Также работает для slice (обнуляет элементы). Память buckets не освобождает.
6. Practice — задачки на проверку
Заголовок раздела «6. Practice — задачки на проверку»Задача 1: Подсчёт уникальных слов
Заголовок раздела «Задача 1: Подсчёт уникальных слов»Напишите функцию, считающую количество уникальных слов в тексте.
func CountUniqueWords(text string) int { // ???}Решение:
import "strings"
func CountUniqueWords(text string) int { words := strings.Fields(text) seen := make(map[string]struct{}, len(words)) for _, w := range words { seen[w] = struct{}{} } return len(seen)}struct{} — оптимальный value для set (0 байт). Можно map[string]bool, но struct{} экономнее.
Задача 2: Группировка
Заголовок раздела «Задача 2: Группировка»Сгруппируйте слайс по результату функции.
func GroupBy[T any, K comparable](s []T, f func(T) K) map[K][]T { // ???}Решение:
func GroupBy[T any, K comparable](s []T, f func(T) K) map[K][]T { out := make(map[K][]T) for _, v := range s { k := f(v) out[k] = append(out[k], v) } return out}
// Использование:nums := []int{1, 2, 3, 4, 5, 6}byParity := GroupBy(nums, func(x int) string { if x%2 == 0 { return "even" } return "odd"})⚠️ Тут используется тот факт, что append к nil-slice работает. out[k] для нового k — это nil, и append создаёт slice.
Задача 3: Найти top-N частых
Заголовок раздела «Задача 3: Найти top-N частых»func TopN(words []string, n int) []string { // вернуть top-N самых частых, в порядке убывания частоты}Решение:
import "sort"
func TopN(words []string, n int) []string { freq := make(map[string]int) for _, w := range words { freq[w]++ } type pair struct { word string count int } pairs := make([]pair, 0, len(freq)) for w, c := range freq { pairs = append(pairs, pair{w, c}) } sort.Slice(pairs, func(i, j int) bool { return pairs[i].count > pairs[j].count }) if n > len(pairs) { n = len(pairs) } res := make([]string, 0, n) for _, p := range pairs[:n] { res = append(res, p.word) } return res}Задача 4: Thread-safe cache
Заголовок раздела «Задача 4: Thread-safe cache»Реализуйте thread-safe cache с TTL.
type Cache struct { mu sync.RWMutex data map[string]cacheItem}
type cacheItem struct { value any exp time.Time}
func (c *Cache) Set(k string, v any, ttl time.Duration) { c.mu.Lock() defer c.mu.Unlock() if c.data == nil { c.data = make(map[string]cacheItem) } c.data[k] = cacheItem{v, time.Now().Add(ttl)}}
func (c *Cache) Get(k string) (any, bool) { c.mu.RLock() defer c.mu.RUnlock() item, ok := c.data[k] if !ok || time.Now().After(item.exp) { return nil, false } return item.value, true}⚠️ Это не cleanup истекших — для production добавьте отдельную goroutine.
Задача 5: Угадайте вывод
Заголовок раздела «Задача 5: Угадайте вывод»m := map[string]int{"a": 1, "b": 2}for k, v := range m { fmt.Println(k, v) delete(m, k) m["c"] = 3 // добавляем!}fmt.Println(m)Решение: Поведение не определено спецификацией для добавления новых ключей во время range.
Возможные исходы:
"a" 1, "b" 2потомm = map[c:3](нашли только начальные)"a" 1, "b" 2, "c" 3(новый ключ попал в итерацию)- Любой другой порядок.
Урок: не делайте так.
7. Источники и дополнительно
Заголовок раздела «7. Источники и дополнительно»- Go Blog — “Go maps in action” — https://go.dev/blog/maps — официальный гайд.
- runtime/map.go — https://github.com/golang/go/blob/master/src/runtime/map.go — исходники с комментариями (must-read для понимания).
- Keith Randall — “Inside the Map Implementation” (GopherCon 2016) — https://www.youtube.com/watch?v=Tl7mi9QmLns — глубокое погружение.
- Dave Cheney — “How the Go runtime implements maps efficiently” — https://dave.cheney.net/2018/05/29/how-the-go-runtime-implements-maps-efficiently-without-generics
- sync.Map docs — https://pkg.go.dev/sync#Map — обязательно прочитайте про use cases.
- Habr — “Maps в Go под капотом” — есть свежие статьи 2024-2025.
- “100 Go Mistakes and How to Avoid Them” (Teiva Harsanyi) — раздел про maps.
- “Concurrent Map Implementations Compared” — статьи сравнения sync.Map с альтернативами (e.g., concurrent-map).
Карта — это не словарь Python. Помнить про buckets, рост, рандомизацию итерации, concurrent access — must для джуна.