Создание и использование мапы
Тип мапы записывается как 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 в тех конкретных случаях, для которых она создана.