Мапы в Go: на что смотреть новичку
Нулевая мапа, паника при записи, случайный порядок итерации и гонки при конкурентном доступе — четыре ловушки, в которые попадает почти каждый, кто начинает писать на Go. Разбираем каждую с примерами и способами защиты.

Почему очевидный код падает
Go подкупает простотой: горутины, строгая типизация, быстрая компиляция. Но map — одна из тех конструкций, где интуиция, перенесённая из Python или JavaScript, работает против вас. Объявили переменную, записали значение — и получили panic: assignment to entry in nil map. Ошибка возникает на этапе выполнения, компилятор её не ловит. Ниже — четыре ситуации, в которых мапы ведут себя не так, как ожидаешь, и конкретные приёмы, как с этим жить.
Нулевая мапа: чтение работает, запись — нет
Переменная типа map[K]V без инициализации равна nil. Чтение из неё не вызывает паники — возвращается нулевое значение типа V. А вот запись роняет программу мгновенно.
// Go 1.21+
package main
func main() {
var scores map[string]int
// Чтение — ок, вернёт 0
_ = scores["alice"]
// Запись — panic: assignment to entry in nil map
scores["alice"] = 42
}
Решение — всегда инициализировать мапу перед записью. Два способа:
scores := make(map[string]int)— пустая мапа, готова к записи;scores := map[string]int{"alice": 42}— литерал с начальными данными.
Если функция принимает мапу как аргумент — проверяйте на nil перед записью или документируйте, что вызывающий код обязан передать инициализированную мапу.
Проверка наличия ключа
Поскольку чтение несуществующего ключа не вызывает ошибки, а возвращает нулевое значение, вы не отличите «ключ есть, значение 0» от «ключа нет». Для этого в Go используется идиома с двумя возвращаемыми значениями:
value, exists := scores["bob"]
if !exists {
// ключа "bob" в мапе нет
}
Игнорировать второй результат — частая ошибка. Если логика зависит от наличия ключа, а не только от значения, всегда используйте двойное присваивание.
Порядок итерации: его нет
Спецификация Go прямо говорит: порядок обхода мапы через range не определён и намеренно рандомизирован между запусками. Это не баг — это защита от кода, который случайно полагается на порядок вставки.
config := map[string]string{
"host": "localhost",
"port": "5432",
"db": "app",
}
for key, val := range config {
fmt.Printf("%s=%s\n", key, val)
}
// При каждом запуске строки могут идти в разном порядке
Если порядок важен — например, при генерации конфигурационного файла или детерминированном выводе — собирайте ключи в слайс, сортируйте его и итерируйтесь по слайсу:
import "sort"
keys := make([]string, 0, len(config))
for k := range config {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Printf("%s=%s\n", k, config[k])
}
Конкурентный доступ: мапа не потокобезопасна
Одновременное чтение и запись в обычную мапу из разных горутин — это гонка данных. Начиная с Go 1.6 рантайм обнаруживает такие гонки и роняет программу с сообщением fatal error: concurrent map read and map write. Это не паника, которую можно перехватить через recover, — процесс завершается безусловно.
Три способа защиты
| Способ | Когда подходит | Цена |
|---|---|---|
sync.Mutex | Много записей, мало горутин | Блокирует все операции на время записи |
sync.RWMutex | Много чтений, редкие записи | Читатели не блокируют друг друга, но писатель ждёт всех |
sync.Map | Ключи стабильны, пишутся один раз и читаются из многих горутин | Нет обобщённых типов — значения any, нужны приведения типов |
Пример с sync.RWMutex:
import "sync"
type SafeScores struct {
mu sync.RWMutex
scores map[string]int
}
func NewSafeScores() *SafeScores {
return &SafeScores{scores: make(map[string]int)}
}
func (s *SafeScores) Set(name string, val int) {
s.mu.Lock()
defer s.mu.Unlock()
s.scores[name] = val
}
func (s *SafeScores) Get(name string) (int, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
v, ok := s.scores[name]
return v, ok
}
Запустите код с флагомgo run -race .— детектор гонок покажет проблему до того, как она выстрелит в продакшене. Привычка гонять тесты с-raceэкономит часы отладки.
Удаление и размер
Удаление элемента — встроенная функция delete(m, key). Если ключа нет, вызов ничего не делает и не паникует. Но есть нюанс: удаление элементов не уменьшает выделенную под мапу память. Go не сжимает внутренние бакеты* при удалении. Если вы наполнили мапу миллионом записей, а потом удалили 999 000 — память останется занятой. Единственный способ её вернуть — создать новую мапу и скопировать нужные ключи.
* Бакет (bucket) — внутренняя единица хранения хеш-таблицы в Go. Каждый бакет хранит до 8 пар ключ-значение. Мапа растёт, добавляя новые бакеты, но не сжимается при удалении.
Что можно и что нельзя использовать как ключ
Ключом мапы может быть любой сравнимый тип: числа, строки, указатели, структуры из сравнимых полей, массивы фиксированной длины. Нельзя — слайсы, другие мапы и функции. Попытка использовать несравнимый тип вызовет ошибку компиляции.
Структуры в качестве ключей удобны для составных ключей — например, пара struct{ UserID int; Day string } вместо склейки строк вида "123:2026-09-23". Компилятор проверит типы, а вы избавитесь от парсинга.
Чего не стоит делать
- Брать адрес значения в мапе:
&m[key]не компилируется. Go может переместить значение при росте мапы, поэтому указатель был бы невалидным. - Полагаться на порядок — повторим, потому что это самая частая причина нестабильных тестов.
- Передавать
nil-мапу в функцию, которая пишет в неё, надеясь, что «внутри разберутся». - Использовать мапу как множество без явного типа значения. Идиоматичный вариант —
map[string]struct{}: пустая структура не занимает памяти.
Что делать дальше
Освоив базовые ловушки, стоит двигаться в двух направлениях. Первое — посмотрите, как обобщённые типы (generics, начиная с Go 1.18) позволяют писать типобезопасные обёртки вокруг мап без any. Второе — запускайте бенчмарки: встроенная мапа хороша для большинства задач, но при миллионах ключей и критичной задержке стоит замерить альтернативы из сторонних библиотек и сравнить с результатами sync.Map. Измеряйте командой go test -bench=. -benchmem — она покажет и аллокации, и время на операцию.
Аналитик звонков: Разбор записи звонка по этапам сделки: где сорвалось, что упущено, три исправляемые рекомендации с цитатами.
Спросить за 90 ₽Источники: The Go Programming Language Specification — Map types, Go Blog — Go maps in action
Частые вопросы
Почему запись в nil-мапу вызывает панику, а чтение — нет?
Можно ли использовать слайс как ключ мапы?
Как сделать мапу потокобезопасной?
Что будет, если удалять элементы из мапы во время итерации range?
Как понять, что в коде есть гонка при доступе к мапе?
Освобождается ли память после удаления большого числа ключей из мапы?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.