Map в Go называют словарём или ассоциативным массивом. Она хранит пары «ключ → значение». В разговорной речи также встречается название «мапа». Через неё удобно находить пользователя по ID, считать повторения и группировать записи.
Разберём создание map, проверку наличия ключа, удаление и обход. Затем посмотрим на ошибки, которые компилятор пропускает: запись в nil, неожиданное изменение общей map и доступ из нескольких горутин.
Основные операции с map
Тип записывается как map[K]V, где K задаёт тип ключа, а V задаёт тип значения. Например, map[string]int связывает строку с целым числом.
Полная программа с основными операциями:
package main
import "fmt"
func main() {
counts := make(map[string]int)
counts["go"] = 1
counts["go"]++
counts["postgresql"] = 1
value, ok := counts["go"]
fmt.Println(value, ok) // 2 true
fmt.Println(len(counts)) // 2
delete(counts, "postgresql")
fmt.Println(len(counts)) // 1
}
Присваивание по существующему ключу заменяет его значение. Двух записей с одинаковым ключом в одной map быть не может. Поэтому len возвращает число уникальных ключей, а не количество присваиваний.
Далее в коротких фрагментах опущены main и импорты. Для вывода нужен пакет fmt, для функций maps и slices нужны одноимённые пакеты.
Краткая шпаргалка:
| Операция | Код | Результат |
|---|---|---|
| Создать пустую map | m := make(map[string]int) | Map готова к записи |
| Добавить или заменить значение | m["go"] = 10 | По ключу "go" хранится 10 |
| Прочитать | value := m["go"] | Значение либо нулевое значение типа |
| Проверить наличие | value, ok := m["go"] | ok показывает, есть ли ключ |
| Удалить | delete(m, "go") | Ключ удалён, если существовал |
| Узнать размер | len(m) | Количество пар |
| Удалить все пары | clear(m) | Map становится пустой |
clear доступен начиная с Go 1.21.
Как создать и инициализировать map в Go
Если начальные данные известны, используйте литерал:
ports := map[string]int{
"http": 80,
"https": 443,
}
ports["postgresql"] = 5432
Для заполнения в цикле обычно удобнее make. Когда примерное число записей известно заранее, его можно передать вторым аргументом:
users := make(map[int]string, 1000)
users[42] = "Анна"
Число 1000 служит подсказкой для начального выделения памяти. Оно не добавляет элементы и не ограничивает размер. Сразу после make длина равна нулю, а при необходимости map вырастет. Встроенную функцию cap к map применять нельзя.
Чем nil отличается от пустой map
Объявление через var не создаёт готовую к записи map:
var counts map[string]int
fmt.Println(counts == nil) // true
fmt.Println(len(counts)) // 0
fmt.Println(counts["go"]) // 0
delete(counts, "go") // Допустимо.
clear(counts) // Допустимо.
Чтение из nil возвращает нулевое значение типа элемента. Удаление и очистка ничего не делают. Цикл range не выполнит ни одной итерации.
Ошибка assignment to entry in nil map
Запись в неинициализированную map вызывает панику:
var counts map[string]int
counts["go"] = 1 // panic: assignment to entry in nil map
Перед записью нужна инициализация:
counts := make(map[string]int)
counts["go"] = 1
Литерал map[string]int{} тоже создаёт пустую map, готовую к записи. У неё и у nil одинаковая длина, но разное поведение при добавлении элементов.
| Действие | var m map[string]int | m := map[string]int{} |
|---|---|---|
m == nil | true | false |
len(m) | 0 | 0 |
m["x"] | 0 | 0 |
m["x"] = 1 | Паника | Добавление элемента |
Подробнее о нулевых значениях типов есть в статье Zero value в Go.
Как проверить наличие ключа в map: value, ok
Одного значения недостаточно, чтобы понять, существует ли запись:
stock := map[string]int{
"book": 0,
}
fmt.Println(stock["book"]) // 0
fmt.Println(stock["pen"]) // 0
Книга есть в каталоге, но закончилась. Ручки в каталоге нет. Обычное чтение возвращает 0 в обоих случаях.
Для различения используйте форму с двумя результатами:
quantity, ok := stock["book"]
fmt.Println(quantity, ok) // 0 true
quantity, ok = stock["pen"]
fmt.Println(quantity, ok) // 0 false
ok относится к наличию ключа. Он остаётся true, даже если значение равно 0, false, пустой строке или nil.
У встроенной map нет метода contains. Если нужно только проверить наличие ключа, отбросьте значение:
if _, ok := stock["book"]; ok {
fmt.Println("Товар есть в каталоге")
}
У нулевого значения есть и полезная сторона. Счётчик не требует отдельной ветки для первого появления слова:
words := []string{"go", "sql", "go"}
counts := make(map[string]int)
for _, word := range words {
counts[word]++
}
fmt.Println(counts["go"]) // 2
При первом обращении чтение даёт 0, затем увеличение записывает 1. Сама map при этом должна быть инициализирована.
Как удалить элемент из map и очистить её
delete удаляет пару по ключу. Отсутствующий ключ не считается ошибкой:
sessions := map[string]int{
"alice": 10,
"bob": 20,
}
delete(sessions, "alice")
delete(sessions, "unknown")
fmt.Println(len(sessions)) // 1
Функция ничего не возвращает. Если нужно узнать, была ли запись, прочитайте её с ok перед удалением. При конкурентном доступе чтение и удаление должны находиться под одной блокировкой.
clear удаляет все пары, сохраняя возможность записи в эту map:
clear(sessions)
fmt.Println(len(sessions)) // 0
sessions["carol"] = 30
Очистка не даёт гарантии немедленного возврата памяти операционной системе. Если большая map больше не нужна, можно убрать ссылку на неё: sessions = nil. Если нужна новая пустая map, используйте sessions = make(map[string]int). Старая станет доступна для сборки мусора, когда на неё не останется ссылок.
Присваивание новой map и clear различаются при наличии других переменных, которые ссылаются на ту же map. Это пригодится в разделе про копирование.
Обход map через range и сортировка ключей
Цикл получает ключ и значение каждой записи:
ports := map[string]int{
"http": 80,
"https": 443,
}
for name, port := range ports {
fmt.Println(name, port)
}
Если нужны только ключи, достаточно for name := range ports. Если только значения, используйте for _, port := range ports.
Порядок обхода не определён. Он не обязан совпадать ни с порядком вставки, ни с предыдущим проходом. Если тест проверяет последовательность строк из range, он может нестабильно падать.
Как получить все ключи и значения map
Чтобы преобразовать ключи или значения map в слайс, используйте maps.Keys или maps.Values вместе с slices.Collect. Пример для Go 1.23 и новее:
ports := map[string]int{
"http": 80,
"https": 443,
}
keys := slices.Collect(maps.Keys(ports))
values := slices.Collect(maps.Values(ports))
fmt.Println(len(keys)) // 2
fmt.Println(len(values)) // 2
Обе функции пакета maps возвращают итераторы. slices.Collect собирает результат в слайс. Порядок элементов не определён. Нельзя сопоставлять keys[i] и values[i]: два независимых обхода могут выдать записи в разном порядке. Если нужны пары, получайте ключ и значение в одном цикле range.
Как отсортировать map по ключам
Для упорядоченного вывода сначала соберите и отсортируйте ключи. Полная программа для Go 1.23 и новее:
package main
import (
"fmt"
"maps"
"slices"
)
func main() {
ports := map[string]int{
"https": 443,
"http": 80,
}
keys := slices.Sorted(maps.Keys(ports))
for _, name := range keys {
fmt.Println(name, ports[name])
}
}
Здесь maps.Keys возвращает итератор, а slices.Sorted собирает его значения в отсортированный слайс. В результате http всегда выводится перед https. Для сортировки по значениям удобнее собрать слайс структур с ключом и значением, затем отсортировать его.
Можно ли удалять во время range
В пределах одной горутины удалять элементы во время обхода разрешено:
counts := map[string]int{
"go": 2,
"sql": 0,
}
for key, count := range counts {
if count == 0 {
delete(counts, key)
}
}
По спецификации Go, если удалить ещё не посещённую запись, цикл её уже не выдаст. Новая запись, добавленная во время обхода, может попасть в обход, а может быть пропущена. Поэтому не стоит строить алгоритм на обязательном посещении добавленных элементов.
Это правило относится к изменениям внутри самого цикла. Оно не разрешает одновременную запись из другой горутины.
Какие типы могут быть ключами
Ключи должны поддерживать операции сравнения == и !=. Строки и целые числа подходят, как и указатели. Массивы и структуры тоже подходят, если их элементы или поля сравнимы.
Например, можно искать данные сразу по двум идентификаторам:
type Key struct {
TenantID int
UserID int
}
names := make(map[Key]string)
names[Key{TenantID: 10, UserID: 42}] = "Анна"
Слайсы, map и функции не подходят для ключей. Объявление map[[]byte]int не скомпилируется. Для байтового содержимого можно выбрать строку, если нужна проверка по содержимому, или массив фиксированного размера, если размер известен.
Интерфейсный ключ требует осторожности. Объявление map[any]int допустимо, но фактическое значение ключа тоже должно быть сравнимым:
values := make(map[any]int)
values["go"] = 1
values[[]int{1, 2}] = 2 // Паника: слайс нельзя использовать как ключ.
Поэтому конкретный тип ключа обычно удобнее: больше ошибок обнаруживается при компиляции. Выбор между вложенной map и структурой в качестве ключа отдельно разобран в статье Составной ключ вместо вложенных map.
Почему поле структуры в map нельзя изменить напрямую
Такой код не скомпилируется:
type User struct {
Visits int
}
users := map[int]User{
42: {Visits: 1},
}
users[42].Visits++ // Ошибка компиляции.
Элемент map не является адресуемым значением. Нельзя взять &users[42] или изменить его поле таким присваиванием. Прочитайте структуру в переменную, измените и запишите обратно:
user := users[42]
user.Visits++
users[42] = user
Если отсутствие пользователя считается ошибкой, чтение нужно дополнить проверкой ok. Иначе код получит нулевую структуру и добавит новую запись.
Другой вариант хранения: map[int]*User. Тогда чтение возвращает указатель, через который можно менять объект:
users := map[int]*User{
42: &User{Visits: 1},
}
if user, ok := users[42]; ok && user != nil {
user.Visits++
}
У этого варианта другая семантика. Несколько записей или других переменных могут ссылаться на одного пользователя. Изменение объекта будет видно через все такие ссылки. Сам указатель в map также может быть nil.
Присваивание map и передача в функцию не копируют записи
После обычного присваивания обе переменные обращаются к одной map:
original := map[string]int{"go": 1}
other := original
other["go"] = 10
fmt.Println(original["go"]) // 10
То же происходит при передаче в функцию. В Go аргументы передаются по значению, но значение map ссылается на общее хранилище записей:
func increment(counts map[string]int, key string) {
counts[key]++
}
Вызов increment(original, "go") изменит запись в исходной map. Указатель *map[string]int для этого не нужен.
Однако присваивание параметру другой map не заменит переменную вызывающего кода:
func reset(counts map[string]int) {
counts = make(map[string]int)
}
Вызов reset(original) оставит исходные записи на месте. Если функция должна удалить их из общей map, используйте clear(counts). Если должна создать замену, верните новую map и присвойте результат снаружи.
Как скопировать map через maps.Clone
Для независимого набора записей есть maps.Clone:
original := map[string]int{"go": 1}
copied := maps.Clone(original)
copied["go"] = 10
fmt.Println(original["go"]) // 1
Пакет maps появился в стандартной библиотеке Go 1.21. Его функция Clone делает поверхностную копию: значения копируются обычным присваиванием. Это не глубокое копирование объектов.
Если значениями являются слайсы, их массивы могут остаться общими:
original := map[string][]int{
"ids": {1, 2},
}
copied := maps.Clone(original)
copied["ids"][0] = 99
fmt.Println(original["ids"][0]) // 99
Для map[string][]int можно отдельно скопировать каждый слайс через slices.Clone:
original := map[string][]int{
"ids": {1, 2},
}
copied := make(map[string][]int, len(original))
for key, value := range original {
copied[key] = slices.Clone(value)
}
copied["ids"][0] = 99
fmt.Println(original["ids"][0]) // 1
Теперь изменения элементов скопированного слайса не затрагивают исходный. Для вложенных map потребуется копирование вложенных map. Глубина копирования зависит от устройства ваших данных.
Как сравнивать map
Оператор == разрешает сравнить map только с nil. Выражение first == second для двух map не скомпилируется.
Для сравнимых значений используйте maps.Equal:
first := map[string]int{"go": 2, "sql": 1}
second := map[string]int{"sql": 1, "go": 2}
fmt.Println(maps.Equal(first, second)) // true
Порядок вставки роли не играет: функция проверяет одинаковые пары ключей и значений. Пустая и nil map для maps.Equal равны.
Если значения нельзя сравнить через ==, задайте правило в maps.EqualFunc. Например, для слайсов чисел:
first := map[string][]int{"ids": {1, 2}}
second := map[string][]int{"ids": {1, 2}}
equal := maps.EqualFunc(first, second, slices.Equal[[]int])
fmt.Println(equal) // true
Доступ из нескольких горутин
Несколько горутин могут читать одну map, если никто её не изменяет. Саму заполненную map нужно передать читателям с корректной синхронизацией, например создать её до запуска горутин.
Ошибка concurrent map writes: как защитить map
Когда есть запись, доступ нужно согласовать. Это относится и к записи по разным ключам. Ошибка может проявиться как fatal error: concurrent map writes или fatal error: concurrent map read and map write. Отсутствие такого сообщения не доказывает корректность программы.
Начать можно с обычной map и sync.Mutex:
package main
import (
"fmt"
"sync"
)
type Counter struct {
mu sync.Mutex
counts map[string]int
}
func (c *Counter) Add(key string) {
c.mu.Lock()
defer c.mu.Unlock()
if c.counts == nil {
c.counts = make(map[string]int)
}
c.counts[key]++
}
func (c *Counter) Get(key string) int {
c.mu.Lock()
defer c.mu.Unlock()
return c.counts[key]
}
func main() {
var counter Counter
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Add("go")
}()
}
wg.Wait()
fmt.Println(counter.Get("go")) // 100
}
Блокировка охватывает всю операцию увеличения счётчика: чтение, прибавление и запись. Если защитить чтение и запись по отдельности, горутины смогут потерять обновления. Одна прочитает старое число до того, как другая запишет новое.
Все обращения к counts должны соблюдать одну схему блокировки. Проверка len, обход и копирование тоже требуют защиты, когда map может изменяться параллельно. Возвращать саму внутреннюю map из метода опасно: вызывающий код сможет обойти блокировку.
Методы используют указатель *Counter, чтобы не копировать мьютекс. Эту ошибку подробнее разбираю в статье Копирование mutex в Go.
Для поиска гонок запускайте тесты с детектором:
$ go test -race ./...
Детектор проверяет выполнявшиеся пути кода. Тесты должны запускать конкурентные обращения, иначе детектор их не проверит.
sync.Map тоже поддерживает конкурентный доступ, но не делает составные действия автоматически атомарными. Например, отдельные Load и Store не превращаются в атомарное увеличение счётчика. Документация sync.Map рекомендует большинству программ обычную map с отдельной синхронизацией. Выбор между Mutex, RWMutex и sync.Map зависит от операций и нагрузки; для базового счётчика достаточно показанного решения.
Вложенные map нужно создавать отдельно
Инициализация внешней map не создаёт внутренние:
groups := make(map[string]map[int]string)
groups["admins"][42] = "Анна" // Паника: внутренняя map равна nil.
Перед первой записью создайте map для группы:
groups := make(map[string]map[int]string)
if groups["admins"] == nil {
groups["admins"] = make(map[int]string)
}
groups["admins"][42] = "Анна"
При чтении вложенных map всё иначе. Выражение groups["unknown"][42] вернёт пустую строку: первое чтение даст nil, второе получит нулевое значение из этой nil map. Если нужно отличить отсутствующую группу от отсутствующего пользователя, проверяйте ok на каждом уровне.
Для группировки в слайсы отдельная инициализация каждого значения обычно не нужна:
groups := make(map[string][]string)
groups["admins"] = append(groups["admins"], "Анна")
Здесь отсутствующий ключ возвращает nil-слайс, а append умеет добавлять элементы в такой слайс. Результат нужно записать обратно в map.
Когда нужна map[string]any
Тип map[string]any позволяет хранить значения разных типов по строковым ключам:
attributes := map[string]any{
"name": "Анна",
"visits": 3,
}
name, ok := attributes["name"].(string)
fmt.Println(name, ok) // Анна true
any обозначает тот же тип, что и interface{}. При чтении результат имеет тип any; чтобы работать с ним как со строкой, пример использует утверждение типа с проверкой ok. Для объектов с заранее известными полями обычно удобнее структура.
Такая map бывает полезна при разборе произвольного JSON. Преобразование map в JSON через json.Marshal и обратный разбор через json.Unmarshal показаны в статье JSON в Go: encoding/json, теги структур и omitempty.
Итог
Для обычного справочника начните с make(map[K]V) или литерала. При чтении выбирайте между одним результатом и value, ok: это зависит от того, нужно ли отличать отсутствие ключа от нулевого значения.
Перед исправлением странного поведения проверьте, инициализирована ли map, кто ещё на неё ссылается и есть ли конкурентная запись. Присваивание не создаёт независимую копию, range не гарантирует порядок, а блокировка должна защищать операцию целиком. Эти правила объясняют большинство ошибок из примеров выше.
Документация
Теги: