Перейти к содержимому
Теория · Map · урок 1 из 9

map

Чему вы научитесь

Чему вы научитесь:

  • хранить пары «ключ — значение» в map и находить значение по ключу;
  • отличать отсутствующий ключ от нулевого значения;
  • обходить map в предсказуемом порядке;
  • объяснять, почему запись в nil-map вызывает панику, а чтение — нет.
Открытая телефонная книга: в строке Anna рядом с именем стоит номер 12-34-56

Представьте телефонную книгу. Вы ищете в ней не «седьмую запись сверху», а имя: находите «Анна» — и рядом видите её номер. Имя здесь — ключ, по которому ищут, а номер — значение, которое находят. Листать книгу целиком ради одного номера не нужно.

Теперь посмотрим, как та же задача выглядит без такой книги. Цены товаров можно хранить в двух срезах: в одном названия, в другом цены. Чтобы узнать цену груши, приходится пройти первый срез с начала и запомнить номер найденного элемента. Для таких данных в Go есть map — набор пар, в котором по ключу сразу находится значение:

GO
package main

import "fmt"

func main() {
    prices := map[string]int{
        "pear": 70,
        "plum": 40,
    }
    fmt.Println(prices["pear"]) // 70
    prices["kiwi"] = 90
    fmt.Println(len(prices)) // 3
}

Давайте разберём пример по строкам:

  1. Литерал создаёт map с двумя парами: груша стоит 70, слива — 40.
  2. prices["pear"] находит цену груши, и программа печатает 70.
  3. prices["kiwi"] = 90 добавляет третью пару, поэтому len возвращает 3.

Тип и литерал

Тип map[string]int читается по частям:

  • map — ключевое слово: «набор пар ключ — значение»;
  • [string] — ключи типа string;
  • int — значения типа int.

Такая запись map прямо в коде — тоже литерал: в его фигурных скобках перечислены пары «ключ: значение». Значение по ключу читают и записывают в квадратных скобках, как элемент среза: prices["pear"].

Запись по ключу

Запись prices[k] = v подчиняется таким правилам:

  • запись по новому ключу добавляет пару;
  • запись по существующему ключу заменяет её значение;
  • каждый ключ встречается в map один раз;
  • функция len возвращает число пар.

Здесь map строже телефонной книги: в книге могут оказаться две записи «Анна», а в map второй такой ключ не появится. Ключом может быть строка, число или bool, но не срез: тип map[[]int]string не скомпилируется.

Пустая map

Пустую map можно создать двумя способами. Результат у них одинаковый: map без пар, в которую можно сразу писать.

Функция make

Вызов make(map[string]int) создаёт пустую map, как make создаёт срез. В скобках — только тип map. Вторым аргументом можно подсказать, сколько пар вы ожидаете: make(map[string]int, 100). Это не ограничение: map вырастет и дальше.

make выбирают, когда map начинается пустой и заполняется позже, например в цикле.

Литерал без пар

Литерал map[string]int{} даёт тот же результат, что и make(map[string]int). Фигурные скобки здесь пустые, но в них можно сразу перечислить пары, как в первом примере. Поэтому литерал удобен, когда часть пар известна заранее.

Быстрый поиск

При поиске по ключу Go не перебирает пары по одной, как цикл по срезу. Время чтения почти не зависит от того, сколько пар хранит map: десять или миллион. Поэтому map выбирают, когда значения нужно часто находить по имени, номеру или другому ключу.

Go считает по ключу число — хеш — и по нему решает, куда положить пару. Добавляйте пары кнопкой и следите, что случится, когда девятой паре не хватит места.

ВживуюMap: как хранятся парыlen = 8 · групп: 1 · ячеек: 8 · до роста: 0

В map 8 пар. Пока их не больше 8, map — одна группа на 8 ячеек, и пары лежат подряд. Нажмите кнопку.

Следите за девятой парой

Пары лежат в группах по восемь ячеек. Когда свободных ячеек становится мало, map удваивает их число и раскладывает все пары заново по хешу.

Отсутствующий ключ

Если имени в телефонной книге нет, map не сообщает об ошибке. Чтение по отсутствующему ключу возвращает нулевое значение типа значений:

  • 0 для int;
  • "" для string.

Удаление пары

Встроенная функция delete удаляет пару по ключу, а для отсутствующего ключа ничего не делает:

GO
stock := make(map[string]int)
stock["pear"] = 3
fmt.Println(stock["kiwi"]) // 0
delete(stock, "pear")
delete(stock, "lime")
fmt.Println(len(stock)) // 0

Давайте разберём пример по шагам:

  1. Киви на складе никогда не было, поэтому чтение stock["kiwi"] возвращает 0.
  2. Первый delete удаляет грушу.
  3. Второй delete ищет лайм, не находит и ничего не делает.
  4. В map не остаётся ни одной пары, и len печатает 0.

Счётчик без подготовки

Счётчик не нужно заводить заранее благодаря нулевому значению:

GO
counts := map[string]int{}
counts["go"]++
counts["go"]++
fmt.Println(counts["go"]) // 2

Разберём два вызова ++ по порядку:

  1. Для нового ключа counts["go"]++ читает 0, прибавляет единицу и записывает 1.
  2. Следующий ++ для того же ключа читает уже 1 и записывает 2.

Частая ошибка

В литерале один ключ написан дважды:

GO
prices := map[string]int{
    "pear": 70,
    "pear": 75, // ✗
}
fmt.Println(prices)
TEXT
duplicate key "pear" in map literal

Компилятор проверяет ключи литерала и не решает за вас, какая цена верная. Программа не соберётся, пока вы не исправите повтор. Оставьте одну пару, а чтобы поменять цену позже, присвойте новое значение: prices["pear"] = 75. Такое присваивание после литерала — не ошибка: оно заменяет значение, как любая запись по существующему ключу.

Главное

  • map[K]V хранит пары «ключ — значение»; значение находят по ключу в квадратных скобках.
  • Запись по новому ключу добавляет пару, по существующему — заменяет значение; каждый ключ встречается один раз.
  • Отсутствующий ключ читается как нулевое значение, поэтому counts[k]++ работает и для нового ключа.
  • Пустую map создают через make(map[string]int) или литерал map[string]int{}.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом