Перейти к основному содержимому

Map (Хеш-таблицы)

map — это встроенная структура данных, представляющая собой неупорядоченную коллекцию пар «ключ-значение». В основе map лежит хеш-таблица.

1. Основные свойства

  • Тип ссылки: Как и слайсы, map передается по ссылке (точнее, это указатель на структуру hmap).
  • Неупорядоченность: Порядок итерации по map рандомизирован. Нельзя полагаться на то, что элементы будут возвращаться в порядке добавления.
  • Нулевое значение: Нулевое значение для map — nil.
    • Из nil мапы можно читать (вернет zero-value типа значения).
    • В nil мапу нельзя записывать (вызовет panic).

2. Внутреннее устройство (Под капотом)

В Go map — это указатель на структуру hmap (header of map), которая содержит:

  1. Количество элементов (count).
  2. Количество "корзин" (buckets) — это массив, где хранятся данные.
  3. Hash seed — случайное число для рандомизации хеш-функции (защита от Hash DoS атак).

Как устроена корзина (Bucket)?

Каждая корзина (bmap) хранит до 8 пар ключ-значение.

  • Если в одну корзину попадает больше 8 элементов, создается overflow bucket (связанный список корзин).
  • Внутри корзины сначала лежат 8 хешей (tophash), затем 8 ключей, затем 8 значений. Это сделано для экономии памяти из-за выравнивания (alignment).

3. Требования к ключу

Ключом в map может быть любой тип, для которого определена операция сравнения (== и !=):

  • Можно: string, int, float, bool, указатели, каналы, интерфейсы, а также структуры и массивы, если они состоят из сравнимых типов.
  • Нельзя: слайсы, мапы, функции (эти типы нельзя сравнивать между собой).

4. Инициализация и емкость

Как и в слайсах, для map можно (и нужно) указывать начальную емкость, если она известна:

// Без указания емкости
m := make(map[string]int)

// С указанием емкости (hint)
m := make(map[string]int, 100)

Зачем указывать емкость? Когда map растет, происходит рехеширование (эвакуация данных):

  1. Создается новый массив корзин в 2 раза больше старого.
  2. Данные постепенно переносятся из старых корзин в новые.
  3. Это дорогая операция, нагружающая CPU и память. Предварительное указание cap позволяет избежать лишних переаллокаций.

5. Операции с Map

m := make(map[string]int)

// Запись
m["key"] = 10

// Чтение (запятая-ок идиома)
val, ok := m["key"]
if ok {
fmt.Println("Значение найдено:", val)
}

// Удаление
delete(m, "key") // Если ключа нет, ничего не произойдет

6. Конкурентность (Потокобезопасность)

Обычная map не является потокобезопасной!

  • Если одна горутина пишет в map, а другая одновременно читает или пишет, возникнет фатальная ошибка: fatal error: concurrent map writes.
  • Решение:
    1. Использовать sync.RWMutex для блокировки доступа.
    2. Использовать sync.Map (специальная реализация для специфических сценариев, когда ключи редко добавляются, но часто читаются).

Вопросы для самопроверки:

  1. Что будет, если достать ключ, которого нет в мапе? Вернется zero-value типа значения (например, 0 для int или "" для string). Чтобы отличить "ноль" от "отсутствия значения", используй val, ok := m[key].
  2. Можно ли взять адрес элемента мапы? Нет, &m["key"] не сработает, потому что при рехешировании элементы перемещаются в памяти, и адрес станет невалидным.
  3. Как удаление влияет на память? delete не освобождает память сразу. Она просто помечает ячейку в корзине как пустую. Если мапа сильно разрослась, а потом ты удалил всё — она все равно будет занимать много памяти. Чтобы реально освободить память, нужно занулить ссылку на мапу и ждать GC.