Урок курса

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, вы уже знаете.

Проверяем отсутствие ключа и нулевое значение

У map[string]int чтение отсутствующего ключа вернёт 0. Но 0 также может быть реально сохранённым значением. Используйте value, ok := m[key], когда эти случаи различаются. Придумайте таблицу остатков: товар с нулевым остатком и неизвестный товар требуют разных ответов пользователю.

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

Продолжите разбор: Структуры и указатели: объявление, инициализация и передача по адресу.

Задача 1

Что вернёт выражение v, ok := m["missing"] для map m := map[string]int{"a": 1}, где ключ "missing" отсутствует?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 2

Программист пишет var m map[string]int, а затем выполняет m["key"] = 1. Что произойдёт при запуске программы?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 3

Что верно о порядке итерации for k, v := range m, где m — это map?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 4

Что выведет следующая программа? Введите ровно одну строку, которая появится в stdout.

package main

import "fmt"

func main() {
    m := map[string]int{"a": 1, "b": 2}
    delete(m, "a")
    delete(m, "c")
    _, ok1 := m["a"]
    _, ok2 := m["b"]
    fmt.Println(ok1, ok2)
}
Без регистрации · результат не сохраняется

Задача 5

Изменения остатков

На складе ведётся учёт товаров по артикулам: каждая запись сообщает, какой позиции и на сколько единиц изменился остаток. Одна и та же позиция может появляться несколько раз — все такие изменения накапливаются и хранятся единой суммой для каждого артикула.

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

Что нужно сделать

Допишите функцию summarize. Она получает параллельные срезы артикулов и изменений, ключ для удаления и ключ для запроса. Верните количество оставшихся ключей, сумму их значений, значение запрошенного ключа и признак его наличия. Повторные изменения одного артикула нужно накопить. Результат не должен зависеть от порядка обхода map.

Формат ввода

Число операций n, затем n пар ключ изменение. В конце — ключ для удаления и ключ для запроса. Ключи не содержат пробелов.

Формат вывода

Первая строка: keys=K total=S. Вторая: value=V, если ключ существует, иначе missing.

Пример

Ввод:
5
apple 5
banana 0
apple -2
cherry 7
banana 3
cherry banana

Вывод:
keys=2 total=6
value=3
РешениеGo

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 6

Удаление из словаря отмеченных слов

Словарь частот — привычный инструмент везде, где нужно отслеживать, сколько раз встречается то или иное слово: в анализе текста, при составлении статистики, в игровой логике. Такой словарь может быть подготовлен заранее и уже содержать записи, в том числе слова с нулевой частотой — зарезервированные позиции, которые ещё не получили реальных появлений. Когда поступает новый список слов, возможно включающий повторы, каждое появление увеличивает соответствующую частоту в словаре.

После того как частоты обновлены, возникает необходимость убрать из словаря одну конкретную запись — пометить её как более не актуальную. Удаление производится по ключу, и здесь важна точность: нужно отличить ситуацию, когда ключ действительно присутствовал в словаре и был успешно удалён, от ситуации, когда такого ключа в словаре никогда не было. Ноль в качестве значения не означает отсутствия ключа, поэтому проверка строится именно на факте присутствия или отсутствия ключа, а не на его числовом значении.

Программа должна вернуть итоговое состояние словаря со всеми оставшимися частотами и ясно обозначить, состоялось ли удаление — то есть был ли указанный ключ найден и изъят из словаря.

Что уже дано

Начальная map может содержать слово со значением 0; это всё равно существующий ключ.

Что нужно сделать

Реализуйте TrackUnknownWords(words []string, initial map[string]int, remove string) UnknownWordReport. Скопируйте начальные частоты, прибавьте появления слов из words, затем удалите ключ remove. Поле Removed должно быть true, если ключ существовал перед удалением, даже когда его значение было нулём. Поле Counts содержит оставшиеся частоты; порядок map не является частью результата.

Ввод и вывод

Проверяющая система вызывает функцию с подготовленным списком слов, начальными частотами и ключом удаления. Она сравнивает возвращаемые Counts и Removed. Читать stdin и печатать результат не нужно.

РешениеGo

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задачи к уроку

Все задачи курса →

Продолжить с сохранением прогресса

Перейдите к уроку в учебном кабинете.

Перейти к интерактивному уроку