Урок курса
Map: создание, чтение, запись, удаление и проверка ключа
Golang с нуляВ предыдущих уроках мы работали со срезами: добавляли элементы, копировали базовый массив, удаляли по индексу. Срез хорош, когда важен порядок и доступ по позиции. Но если нужно быстро найти значение по имени — по слову, идентификатору, коду — срез заставляет перебирать всё подряд. Map решает именно эту задачу: O(1) доступ по произвольному ключу.
Тип map[K]V, литерал и make: инициализация и nil-паника
Тип map[string]int читается буквально: отображение из string в int. Ключом может быть любой сравниваемый тип — строка, число, булево, указатель; срез или другая map ключом быть не могут.
Создать map можно двумя способами. Первый — литерал с начальными данными:
scores := map[string]int{
"alice": 42,
"bob": 17,
}
Второй — make, когда начальных данных нет:
scores := make(map[string]int)
Оба варианта дают готовую к работе map. Разница лишь в том, есть ли начальные пары.
А вот что будет, если просто объявить переменную без инициализации:
var scores map[string]int
scores["alice"] = 42 // panic: assignment to entry in nil map
Нулевое значение map — nil. Читать из nil-map можно (вернётся нулевое значение типа), но запись вызывает panic немедленно. Это принципиально отличается от nil-среза, в который нельзя записать через индекс, но можно передать в append.
Практическое правило: перед любой записью map должна быть инициализирована — либо литералом, либо через make. Выбор между ними прост: если начальные пары известны заранее — литерал; если map наполняется в процессе работы программы — make.
Чтение, запись, идиома value-ok и delete
Запись и обновление — один синтаксис:
m := make(map[string]int)
m["hits"] = 1 // новая пара
m["hits"] = 2 // перезапись; теперь m["hits"] == 2
Чтение по ключу:
v := m["hits"] // v == 2
v = m["miss"] // ключа нет — v == 0
Вот где возникает проблема. Допустим, вы храните количество ошибок и ещё не фиксировали ни одной. m["errors"] вернёт 0 — но это нулевое значение типа, а не подтверждение, что ключ вообще существует. Отличить одно от другого помогает двухзначная форма:
v, ok := m["errors"]
if ok {
fmt.Println("ключ есть, значение:", v)
} else {
fmt.Println("ключа нет")
}
Если ключ присутствует, ok == true и v — реальное значение. Если ключа нет, ok == false и v == 0. Когда важно только наличие ключа, переменную значения отбрасывают: _, ok := m[key].
Ситуация, где это критично: ключ "errors" был явно добавлен со значением 0 (например, как сброс счётчика). Однозначная форма v := m["errors"] даст 0 в обоих случаях — и когда ключа нет, и когда он есть с нулём. Только v, ok позволяет их различить.
Удаление ключа:
delete(m, "hits")
delete — встроенная функция, не метод. Если ключа в map нет, вызов просто ничего не делает — паники не будет. После удаления v, ok := m["hits"] вернёт ok == false.
Обход map через range и неопределённость порядка итерации
Синтаксис range для map выглядит так же, как для среза, который вы уже использовали:
m := map[string]int{"alice": 3, "bob": 1, "carol": 7}
for k, v := range m {
fmt.Println(k, v)
}
Но есть принципиальное отличие от среза: спецификация Go не задаёт порядок обхода map и не гарантирует, что два последовательных обхода дадут одинаковую последовательность пар. Два прогона могут дать как одинаковый, так и разный порядок — полагаться на него нельзя в любом случае.
Если нужен только ключ:
for k := range m {
fmt.Println(k)
}
Если нужно только значение:
for _, v := range m {
fmt.Println(v)
}
Пустой идентификатор _ работает здесь так же, как и в range по срезу — отбрасывает переменную, которая не нужна.
Практическое следствие: тест, который проверяет порядок строк в выводе, построенном по range по map, ненадёжен и потенциально нестабилен — даже если на конкретной машине он проходит несколько раз подряд. Корректный тест проверяет наличие нужных ключей и значений в map, а не порядок их перечисления.
Подсчёт частот токенов в map без инициализации отдельных ключей
Допустим, у вас есть срез токенов — слов, тегов, идентификаторов — и нужно посчитать, сколько раз встречается каждый:
tokens := []string{"go", "map", "go", "range", "go", "map"}
freq := make(map[string]int)
for _, t := range tokens {
freq[t]++
}
// freq: {"go": 3, "map": 2, "range": 1}
Ключевой момент — строка freq[t]++. Когда токен встречается впервые, ключа t в map ещё нет. freq[t] возвращает нулевое значение int, то есть 0. Затем ++ увеличивает его до 1, и пара сохраняется. При следующем вхождении того же токена ключ уже существует, и счётчик увеличивается с реального значения.
Никакой предварительной проверки if _, ok := freq[t]; !ok { freq[t] = 0 } не нужно — нулевое значение типа делает её избыточной. Это намеренное свойство map в Go, и паттерн freq[t]++ — стандартный способ агрегации.
После заполнения map можно обойти результат через range:
for word, count := range freq {
fmt.Printf("%s: %d\n", word, count)
}
Порядок вывода будет произвольным — что именно ожидать от range по map, вы уже знаете.
Попробуйте решить
Что вернёт выражение v, ok := m["missing"] для map m := map[string]int{"a": 1}, где ключ "missing" отсутствует?
Продолжить с проверкой и прогрессом
Откройте интерактивный раннер с заданиями урока.
