Map (Хеш-таблицы)
map — это встроенная структура данных, представляющая собой неупорядоченную коллекцию пар «ключ-значение». В основе map лежит хеш-таблица.
1. Основные свойства
- Тип ссылки: Как и слайсы,
mapпередается по ссылке (точнее, это указатель на структуруhmap). - Неупорядоченность: Порядок итерации по map рандомизирован. Нельзя полагаться на то, что элементы будут возвращаться в порядке добавления.
- Нулевое значение: Нулевое значение для map —
nil.- Из
nilмапы можно читать (вернет zero-value типа значения). - В
nilмапу нельзя записывать (вызоветpanic).
- Из
2. Внутреннее устройство (Под капотом)
В Go map — это указатель на структуру hmap (header of map), которая содержит:
- Количество элементов (
count). - Количество "корзин" (
buckets) — это массив, где хранятся данные. - 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 растет, происходит рехеширование (эвакуация данных):
- Создается новый массив корзин в 2 раза больше старого.
- Данные постепенно переносятся из старых корзин в новые.
- Это дорогая операция, нагружающая 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. - Решение:
- Использовать
sync.RWMutexдля блокировки доступа. - Использовать
sync.Map(специальная реализация для специфических сценариев, когда ключи редко добавляются, но часто читаются).
- Использовать
Вопросы для самопроверки:
- Что будет, если достать ключ, которого нет в мапе? Вернется
zero-valueтипа значения (например,0дляintили""дляstring). Чтобы отличить "ноль" от "отсутствия значения", используйval, ok := m[key]. - Можно ли взять адрес элемента мапы? Нет,
&m["key"]не сработает, потому что при рехешировании элементы перемещаются в памяти, и адрес станет невалидным. - Как удаление влияет на память?
deleteне освобождает память сразу. Она просто помечает ячейку в корзине как пустую. Если мапа сильно разрослась, а потом ты удалил всё — она все равно будет занимать много памяти. Чтобы реально освободить память, нужно занулить ссылку на мапу и ждать GC.