Map (словари / хэш-таблицы)
Храним данные парами «ключ — значение»: создаём, читаем, удаляем и перебираем
В прошлом уроке мы изучили массивы и срезы — структуры, в которых элементы хранятся по порядковому номеру (индексу). Это удобно, но далеко не всегда. Представьте телефонную книгу: вы ищете номер не по позиции «третий контакт», а по имени — «Алексей». Или словарь: вы открываете его не на странице 47, а на слове «map». Именно для таких задач существует структура данных map — она же словарь, она же хэш-таблица. Map хранит пары «ключ → значение» и позволяет мгновенно находить значение по ключу. Сегодня мы разберём map от создания до реальной практики — напишем счётчик слов в тексте.
📖 Что такое map и зачем он нужен
Map — это коллекция, где каждый элемент состоит из двух частей: ключа и значения. Ключ — это «имя», по которому вы ищете данные. Значение — это сами данные. Работает это как реальный словарь: вы знаете слово (ключ) и хотите узнать его перевод (значение).
Несколько примеров из жизни, которые идеально ложатся на map:
Телефонная книга: имя → номер телефона
Словарь: слово → перевод
Настройки: название параметра → его значение
Корзина магазина: товар → количество
В отличие от среза, где данные лежат по числовому индексу (0, 1, 2...), в map ключом может быть строка, число или любой сравниваемый тип. Это даёт огромную гибкость.
💡 Подсказка: map в Go — это аналог dict в Python, HashMap в Java или объектов-словарей в JavaScript. Если вы встречали эти структуры в других языках, map в Go покажется вам знакомым.
🛠 Создание map
В Go есть два основных способа создать map. Первый и самый распространённый — с помощью функции make:
package main
import "fmt"
func main() {
// Создаём пустой map: ключи — строки, значения — строки
phonebook := make(map[string]string)
fmt.Println(phonebook) // map[]
}
Разберём синтаксис. make(map[string]string) — мы вызываем функцию make и передаём ей тип: map[string]string. В квадратных скобках указан тип ключа (string), а после скобок — тип значения (string). Получается map, где и ключи, и значения — строки.
Типы ключа и значения могут быть разными. Вот map, где ключ — строка, а значение — число:
// Ключ — строка (название товара), значение — int (цена)
prices := make(map[string]int)
Второй способ — создать map сразу с данными, используя литерал:
package main
import "fmt"
func main() {
// Создаём map с начальными данными
capitals := map[string]string{
"Россия": "Москва",
"Казахстан": "Астана",
"Узбекистан": "Ташкент",
}
fmt.Println(capitals)
// map[Казахстан:Астана Россия:Москва Узбекистан:Ташкент]
}
Обратите внимание: после последнего элемента ("Ташкент") стоит запятая. В Go это обязательно, если закрывающая скобка } стоит на новой строке. Без этой запятой код не скомпилируется.
💡 Подсказка: никогда не объявляйте map через var m map[string]int без инициализации. Такой map будет nil, и попытка записи в него вызовет панику (аварийное завершение программы). Всегда используйте make или литерал.
Вот наглядная демонстрация опасности nil-map:
var m map[string]int // m == nil
// m["ключ"] = 42 // ПАНИКА! assignment to entry in nil map
m = make(map[string]int) // Теперь m инициализирован
m["ключ"] = 42 // Всё работает
➕ Добавление и изменение элементов
Добавить элемент в map очень просто — присваиваем значение по ключу:
package main
import "fmt"
func main() {
phonebook := make(map[string]string)
// Добавляем записи
phonebook["Алексей"] = "+7-900-111-22-33"
phonebook["Мария"] = "+7-900-444-55-66"
phonebook["Дмитрий"] = "+7-900-777-88-99"
fmt.Println(phonebook)
// map[Алексей:+7-900-111-22-33 Дмитрий:+7-900-777-88-99 Мария:+7-900-444-55-66]
}
Синтаксис phonebook["Алексей"] = "+7-900-111-22-33" работает по принципу: если ключа "Алексей" ещё нет — он создаётся. Если такой ключ уже есть — его значение перезаписывается. Одна и та же запись и для добавления, и для обновления:
package main
import "fmt"
func main() {
scores := make(map[string]int)
scores["Алексей"] = 85 // Добавляем
fmt.Println(scores) // map[Алексей:85]
scores["Алексей"] = 92 // Перезаписываем
fmt.Println(scores) // map[Алексей:92]
}
Никаких специальных методов вроде add() или put() — просто присваивание по ключу. Просто и понятно.
📖 Чтение значений из map
Чтобы прочитать значение, обращаемся к map по ключу — точно так же, как к срезу по индексу:
package main
import "fmt"
func main() {
capitals := map[string]string{
"Россия": "Москва",
"Казахстан": "Астана",
"Узбекистан": "Ташкент",
}
fmt.Println(capitals["Россия"]) // Москва
fmt.Println(capitals["Казахстан"]) // Астана
}
А что произойдёт, если мы обратимся по ключу, которого нет? В отличие от среза (где выход за границы вызывает панику), map просто вернёт нулевое значение для своего типа:
fmt.Println(capitals["Германия"]) // "" (пустая строка — нулевое значение для string)
Для map[string]int отсутствующий ключ вернёт 0, для map[string]bool — false и так далее. Это удобно, но может привести к проблеме: как отличить «ключ существует, и его значение равно нулю» от «ключа вообще нет»? Именно для этого существует специальная проверка, которую мы разберём в следующей секции.
🔍 Проверка существования ключа: value, ok
Это один из самых важных приёмов работы с map в Go. При обращении к map можно получить два значения:
value, ok := m[key]
Здесь value — это значение по ключу (или нулевое, если ключа нет), а ok — булево значение: true, если ключ существует, и false, если его нет. Давайте посмотрим на полном примере:
package main
import "fmt"
func main() {
scores := map[string]int{
"Алексей": 92,
"Мария": 0,
}
// Проверяем существующий ключ
value, ok := scores["Алексей"]
fmt.Println(value, ok) // 92 true
// Проверяем ключ со значением 0
value, ok = scores["Мария"]
fmt.Println(value, ok) // 0 true — ключ ЕСТЬ, значение равно 0
// Проверяем несуществующий ключ
value, ok = scores["Дмитрий"]
fmt.Println(value, ok) // 0 false — ключа НЕТ
}
Видите разницу? У Марии оценка 0 и ok == true — ключ существует. У Дмитрия тоже 0, но ok == false — такого ключа нет. Без переменной ok мы бы не смогли их различить.
На практике эту конструкцию чаще всего комбинируют с if:
package main
import "fmt"
func main() {
capitals := map[string]string{
"Россия": "Москва",
"Казахстан": "Астана",
}
country := "Россия"
if capital, ok := capitals[country]; ok {
fmt.Printf("Столица %s — %s\n", country, capital)
} else {
fmt.Printf("Страна %s не найдена\n", country)
}
// Вывод: Столица Россия — Москва
}
Здесь мы использовали if с инициализацией: объявили capital и ok прямо внутри условия, а после точки с запятой проверили ok. Это самый идиоматичный (правильный с точки зрения стиля Go) способ проверки ключа.
💡 Подсказка: если вам не нужно само значение, а только проверить наличие ключа, используйте пустой идентификатор _ вместо первой переменной: _, ok := m[key].
🗑 Удаление элементов: функция delete
Для удаления элемента из map используется встроенная функция delete. Она принимает два аргумента: сам map и ключ, который нужно удалить:
package main
import "fmt"
func main() {
fruits := map[string]int{
"яблоки": 5,
"бананы": 3,
"апельсины": 8,
}
fmt.Println("До удаления:", fruits)
// До удаления: map[апельсины:8 бананы:3 яблоки:5]
delete(fruits, "бананы")
fmt.Println("После удаления:", fruits)
// После удаления: map[апельсины:8 яблоки:5]
}
Функция delete ничего не возвращает. Если вы попробуете удалить ключ, которого нет в map, ничего плохого не произойдёт — Go просто проигнорирует вызов:
delete(fruits, "манго") // Ключа "манго" нет — ничего не происходит, ошибки нет
Это удобно: не нужно предварительно проверять, существует ли ключ перед удалением.
📏 Длина map: функция len
Как и для срезов, функция len возвращает количество элементов в map:
package main
import "fmt"
func main() {
colors := map[string]string{
"red": "красный",
"green": "зелёный",
"blue": "синий",
}
fmt.Println(len(colors)) // 3
delete(colors, "red")
fmt.Println(len(colors)) // 2
}
🔄 Итерация по map: перебираем все элементы
Чтобы обойти все пары «ключ — значение» в map, используется цикл for range — тот же, что и для срезов:
package main
import "fmt"
func main() {
population := map[string]int{
"Москва": 13_000_000,
"Ташкент": 3_000_000,
"Астана": 1_400_000,
}
for city, people := range population {
fmt.Printf("%s: %d жителей\n", city, people)
}
}
На каждой итерации city получает ключ, а people — значение. Но есть важная особенность:
💡 Подсказка: порядок обхода map в Go не гарантирован. При каждом запуске программы элементы могут идти в разном порядке. Это сделано специально. Если вам нужен конкретный порядок — сначала соберите ключи в срез, отсортируйте его, а затем обходите map по отсортированным ключам.
Если вам нужны только ключи, значение можно заменить на _:
// Только ключи
for city := range population {
fmt.Println(city)
}
А если нужны только значения, пропускаем ключ:
// Только значения
for _, people := range population {
fmt.Println(people)
}
Давайте напишем более содержательный пример — посчитаем общую стоимость корзины товаров:
package main
import "fmt"
func main() {
cart := map[string]int{
"Хлеб": 55,
"Молоко": 89,
"Сыр": 320,
"Яйца": 115,
}
total := 0
for product, price := range cart {
fmt.Printf(" %s — %d руб.\n", product, price)
total += price
}
fmt.Printf("Итого: %d руб.\n", total)
}
🧩 Map со срезами в качестве значений
Значением в map может быть что угодно, включая срез. Это позволяет строить более сложные структуры. Например, у каждого студента несколько оценок:
package main
import "fmt"
func main() {
// Ключ — имя студента, значение — срез оценок
grades := map[string][]int{
"Алексей": {5, 4, 5, 5, 4},
"Мария": {5, 5, 5, 4, 5},
"Дмитрий": {3, 4, 4, 3, 5},
}
// Вычисляем средний балл для каждого студента
for name, marks := range grades {
sum := 0
for _, mark := range marks {
sum += mark
}
avg := float64(sum) / float64(len(marks))
fmt.Printf("%s: средний балл %.1f\n", name, avg)
}
}
Тип map[string][]int читается так: map, где ключ — строка, а значение — срез целых чисел. Вложенный цикл for _, mark := range marks перебирает оценки каждого студента.
Также можно добавлять элементы к срезу внутри map:
grades["Алексей"] = append(grades["Алексей"], 5) // Добавили ещё одну оценку
📊 Обход map в отсортированном порядке
Как мы уже говорили, порядок обхода map случаен. Но иногда нам нужен чёткий порядок. Решение: достаём ключи в срез, сортируем его и обходим map по отсортированным ключам:
package main
import (
"fmt"
"sort"
)
func main() {
population := map[string]int{
"Москва": 13_000_000,
"Астана": 1_400_000,
"Ташкент": 3_000_000,
"Минск": 2_000_000,
}
// 1. Собираем ключи в срез
cities := make([]string, 0, len(population))
for city := range population {
cities = append(cities, city)
}
// 2. Сортируем ключи
sort.Strings(cities)
// 3. Обходим map в алфавитном порядке
for _, city := range cities {
fmt.Printf("%s: %d\n", city, population[city])
}
// Астана: 1400000
// Минск: 2000000
// Москва: 13000000
// Ташкент: 3000000
}
Пакет sort мы подробнее рассмотрим позже, но уже сейчас вы видите, как sort.Strings сортирует срез строк в алфавитном порядке. Теперь обход происходит предсказуемо: от А до Я.
🏋️ Практика: счётчик слов в тексте
Теперь давайте применим всё, что узнали, в реальной задаче. Напишем программу, которая считает, сколько раз каждое слово встречается в тексте. Это классическая задача, которая идеально решается с помощью map.
Идея простая: берём текст, разбиваем его на слова, и для каждого слова увеличиваем счётчик в map на единицу. Ключ — слово, значение — количество его появлений.
package main
import (
"fmt"
"strings"
)
func main() {
text := "кот сидел на коврике а коврик лежал на полу кот любил этот коврик"
// Разбиваем текст на слова
words := strings.Fields(text)
// Создаём map для подсчёта
counter := make(map[string]int)
// Считаем каждое слово
for _, word := range words {
counter[word]++
}
// Выводим результат
for word, count := range counter {
fmt.Printf("%-10s — %d раз(а)\n", word, count)
}
}
Разберём ключевые моменты:
strings.Fields(text) — функция из пакета strings, которая разбивает строку на слова по пробелам. Возвращает срез строк: ["кот", "сидел", "на", "коврике", ...].
counter[word]++ — самая элегантная часть. Если ключа word ещё нет в map, Go автоматически создаёт его с нулевым значением (0 для int), и затем увеличивает на 1. Не нужно проверять, есть ли слово в map!
%-10s — форматирование в Printf: выводит строку, выровненную влево, с минимальной шириной 10 символов. Это делает вывод аккуратным столбиком.
Результат будет примерно таким (порядок может отличаться):
кот — 2 раз(а)
сидел — 1 раз(а)
на — 2 раз(а)
коврике — 1 раз(а)
а — 1 раз(а)
коврик — 2 раз(а)
лежал — 1 раз(а)
полу — 1 раз(а)
любил — 1 раз(а)
этот — 1 раз(а)
Обратите внимание: «коврике» и «коврик» считаются как разные слова, потому что у них разная форма. Чтобы их объединить, нужно приводить слова к начальной форме — но это уже задача для продвинутого уровня.
Давайте улучшим нашу программу: приведём все слова к нижнему регистру (чтобы «Кот» и «кот» считались одним словом) и отсортируем вывод:
package main
import (
"fmt"
"sort"
"strings"
)
func main() {
text := "Go это язык программирования Go быстрый Go простой и Go надёжный"
words := strings.Fields(text)
counter := make(map[string]int)
for _, word := range words {
lower := strings.ToLower(word) // Приводим к нижнему регистру
counter[lower]++
}
// Собираем ключи и сортируем
keys := make([]string, 0, len(counter))
for word := range counter {
keys = append(keys, word)
}
sort.Strings(keys)
// Выводим в алфавитном порядке
fmt.Println("=== Частота слов ===")
for _, word := range keys {
fmt.Printf("%-20s %d\n", word, counter[word])
}
fmt.Printf("\nВсего уникальных слов: %d\n", len(counter))
}
Теперь «Go» в начале предложения и «go» в середине будут считаться одним словом. Программа стала надёжнее.
⚠️ Частые ошибки новичков
Разберём типичные ловушки, в которые попадают начинающие разработчики при работе с map.
Ошибка 1: запись в nil-map
var m map[string]int
m["ключ"] = 42 // ПАНИКА: assignment to entry in nil map
Решение: всегда инициализируйте map через make или литерал.
Ошибка 2: полагаться на порядок элементов
// НЕ делайте так! Порядок будет разным при каждом запуске
m := map[int]string{1: "a", 2: "b", 3: "c"}
for k, v := range m {
fmt.Println(k, v) // Порядок непредсказуем!
}
Решение: если нужен порядок, используйте отсортированные ключи (как мы показали выше).
Ошибка 3: сравнение map через ==
a := map[string]int{"x": 1}
b := map[string]int{"x": 1}
// fmt.Println(a == b) // Ошибка компиляции! Map нельзя сравнивать через ==
Map можно сравнить только с nil. Для сравнения двух map нужно вручную пройтись по всем ключам и сравнить значения.
📝 Шпаргалка по map
Собрали все операции с map в одном месте для быстрого повторения:
// Создание
m := make(map[string]int) // Пустой map через make
m := map[string]int{"a": 1, "b": 2} // Литерал с данными
// Добавление / изменение
m["ключ"] = 42
// Чтение
value := m["ключ"]
// Проверка существования ключа
value, ok := m["ключ"]
if ok {
// Ключ существует
}
// Идиоматичная проверка
if value, ok := m["ключ"]; ok {
fmt.Println(value)
}
// Удаление
delete(m, "ключ")
// Длина
fmt.Println(len(m))
// Обход всех элементов
for key, value := range m {
fmt.Println(key, value)
}
// Только ключи
for key := range m {
fmt.Println(key)
}
// Только значения
for _, value := range m {
fmt.Println(value)
}
📋 Итоги урока
- Map хранит данные парами «ключ → значение» и позволяет мгновенно находить значение по ключу
- Создавать map нужно через
make(map[K]V)или литерал — никогда не пишите в nil-map - Добавление и обновление — одна и та же операция:
m[key] = value - Конструкция
value, ok := m[key]позволяет отличить «ключ есть, значение нулевое» от «ключа нет» - Функция
delete(m, key)удаляет элемент; удаление несуществующего ключа безопасно - Порядок обхода map через
for rangeне гарантирован — для сортировки используйте срез ключей - Map идеально подходит для подсчёта частоты:
counter[word]++работает даже для нового ключа - Мы написали счётчик слов — классическую задачу, которая элегантно решается через map
В следующем уроке мы займёмся строками. В Go строки — это не просто текст, а последовательность байтов с поддержкой Unicode. Вы узнаете, как работают руны, чем len для строк отличается от того, что вы ожидаете, и научитесь разбивать, склеивать, искать и заменять части текста с помощью пакета strings. Строки — один из самых используемых типов данных, и после урока 3.3 вы будете чувствовать себя с ними уверенно.