Menu

Map в Golang: создание, проверка ключа, обход, сортировка

Мапы в Go хранят пары ключ-значение с быстрым поиском. Как их создавать, проверять наличие ключа через comma-ok, удалять, обходить (в случайном порядке), сортировать ключи, хранить структуры и избегать паник от nil-мапы и конкурентной записи.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Создание и использование мапы

Тип мапы записывается как map[KeyType]ValueType. Создайте её литералом или через make, а затем читайте, пишите и удаляйте по ключу.

Вывод:

31
3
map[bob:26 cy:40]
2 0

Здесь видны два удобства. Чтение отсутствующего ключа возвращает нулевое значение типа значений (counts['z'] равно 0), поэтому подсчёт через m[k]++ работает без подготовки. А fmt печатает мапы с отсортированными ключами, что удобно при отладке, но ничего не говорит о порядке обхода.

make(map[K]V, n) принимает необязательную подсказку размера. Она заранее выделяет место примерно под n записей; в отличие от слайсов, у мапы нет ёмкости, которую можно прочитать.

Проверка наличия ключа: comma-ok

Поскольку отсутствующий ключ читается как нулевое значение, m[k] == 0 не отличает «нет ключа» от «сохранён 0». Используйте форму с двумя значениями:

Форма if v, ok := m[k]; ok { ... } ограничивает область видимости v и ok этим if. Это одна из самых частых строк в коде на Go.

Удаление записей

delete(m, key) удаляет запись. Удаление отсутствующего ключа ничего не делает, как и удаление из nil-мапы. Чтобы очистить мапу целиком, в Go 1.21 появился clear(m), который сохраняет выделенную мапу для повторного использования.

Удалять записи во время range по той же мапе разрешено и безопасно. Запись, удалённая до того, как цикл до неё дошёл, выдана не будет.

Обход: порядок случайный

for k, v := range m посещает каждую запись один раз в неопределённом порядке. Среда выполнения намеренно рандомизирует начальную точку, поэтому два цикла по одной мапе в одной программе часто расходятся. Запустите это несколько раз:

Любой код, вывод которого зависит от порядка мапы, это баг, ждущий другого запуска. Классический пример: тесты, которые сравнивают напечатанный обход мапы.

Отсортированные ключи

Чтобы обойти мапу в порядке ключей, получите ключи, отсортируйте их и обращайтесь к мапе по ним. В Go 1.23 это стало одной строкой благодаря итераторам из пакетов maps и slices:

В Go 1.22 и раньше maps.Keys в стандартной библиотеке не было. Эквивалент это цикл:

keys := make([]string, 0, len(m))
for k := range m {
	keys = append(keys, k)
}
sort.Strings(keys)

Другие функции пакета maps: maps.Values, maps.Clone (поверхностная копия), maps.Equal, maps.Copy(dst, src) и maps.DeleteFunc.

Допустимые типы ключей

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

map[[]int]bool{}       // compile error: invalid map key type []int
map[[2]int]bool{}      // fine: arrays are comparable
map[struct{ X, Y int }]string{} // fine: a struct key for a grid position

Ключ-структура это идиоматичный способ использовать в качестве ключа сразу несколько значений вместо склеивания строк.

Ключи-интерфейсы компилируются, даже если динамический тип несравним, и тогда паникуют во время выполнения: сохранение []int в map[any]int падает с runtime error: hash of unhashable type []int.

Ключи с плавающей точкой работают, но NaN не равен сам себе, поэтому ключ NaN можно вставлять снова и снова и никогда не прочитать. Избегайте ключей типа float.

Мапа структур

Мапа может хранить структуры, но присвоить значение полю структуры, лежащей в мапе, нельзя, потому что значения мапы неадресуемы.

Выбирайте значения, когда записи маленькие и заменяются целиком. Выбирайте указатели, когда поля часто обновляются или одна запись используется из нескольких мест. С указателями отсутствующий ключ возвращает nil, поэтому ptrs["nope"].Score вызовет панику.

Мапы слайсов с append работают так же: groups[k] = append(groups[k], v) не требует инициализации, потому что отсутствующий ключ даёт nil-слайс, а append умеет работать с nil.

Мапы ведут себя как ссылки

Значение мапы ссылается на общие данные. Присваивание мапы или передача в функцию не копирует записи: обе переменные видят одну и ту же мапу.

Поэтому функция может заполнить мапу, не возвращая её, в отличие от слайса, в который она делает append.

Паника nil-мапы

Нулевое значение мапы это nil. nil-мапа читается как пустая, но запись в неё вызывает панику.

Вывод:

0 0
recovered: assignment to entry in nil map

На практике кусается именно случай со структурой. Инициализируйте поля-мапы в конструкторе (func NewCache() *Cache { return &Cache{data: map[string]string{}} }) или лениво перед первой записью.

Конкурентный доступ

Мапы небезопасны для конкурентного использования. Если одна горутина пишет, пока другая читает или пишет, среда выполнения может остановить программу с fatal error: concurrent map writes (или concurrent map read and map write). Это фатальная ошибка, а не паника, поэтому recover её не перехватит.

Защитите мапу мьютексом:

Программа всегда печатает 50 50. Используйте sync.RWMutex, когда чтений гораздо больше, чем записей. sync.Map существует для двух узких случаев (ключи записываются один раз и читаются много раз, или горутины работают с непересекающимися ключами); во всех остальных мьютекс и обычная мапа проще и обычно быстрее. Подробнее на странице о мьютексе.

Шпаргалка

ОперацияКод
Создатьm := map[string]int{} или make(map[string]int)
Вставить или обновитьm[k] = v
Прочитать (ноль, если нет)v := m[k]
Проверить наличиеv, ok := m[k]
Удалитьdelete(m, k)
Удалить всёclear(m) (Go 1.21)
Размерlen(m)
Отсортированные ключиslices.Sorted(maps.Keys(m)) (Go 1.23)
Копияmaps.Clone(m)
Сравнениеmaps.Equal(a, b)

Мапа со значениями struct{} это ещё и тип множества в Go; см. множества.

Частые ошибки

  • Запись в nil-мапу. Всегда создавайте её через make, включая поля-мапы в структурах.
  • Расчёт на порядок обхода. Сортируйте ключи.
  • m[k] != 0 как проверка наличия. Используйте comma-ok.
  • Изменение поля структуры через m[k].Field. Скопируйте, измените и запишите обратно или храните указатели.
  • Общая мапа для горутин без блокировки. Такой крах не перехватить.

Часто задаваемые вопросы

Как проверить, есть ли ключ в map в Go?

Используйте форму поиска с двумя значениями: v, ok := m[key]. ok равно true, когда ключ есть, и false, когда его нет; в этом случае v содержит нулевое значение. Одним только чтением m[key] нельзя отличить отсутствующий ключ от ключа, сохранённого с нулевым значением.

Почему порядок обхода map в Go случайный?

Язык не определяет порядок, а среда выполнения намеренно начинает каждый range со случайной позиции, чтобы программы не начали от него зависеть. Чтобы обойти мапу в порядке ключей, соберите и отсортируйте ключи: for _, k := range slices.Sorted(maps.Keys(m)) (Go 1.23).

Как получить все ключи map в Go?

Начиная с Go 1.23 maps.Keys(m) возвращает итератор; превратите его в слайс через slices.Collect(maps.Keys(m)) или в отсортированный слайс через slices.Sorted(maps.Keys(m)). До 1.23 пройдите циклом for k := range m и добавляйте каждый ключ в слайс.

Почему запись в map паникует с «assignment to entry in nil map»?

Переменная мапы объявлена, но мапа не создана: var m map[string]int равна nil. Чтение из nil-мапы возвращает нулевые значения, а запись вызывает панику. Сначала создайте её через m = make(map[string]int) или литерал m := map[string]int{}. Поле-мапа внутри структуры требует такой же инициализации.

Безопасны ли мапы в Go для конкурентного использования?

Нет. Конкурентные записи или запись одновременно с чтением могут уронить программу с fatal error: concurrent map writes, и recover это не перехватит. Защищайте мапу через sync.Mutex или sync.RWMutex либо используйте sync.Map в тех конкретных случаях, для которых она создана.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ