Глиф Учебники

Урок 6. Map — словари в Go

Курс «Go» · урок 6 из 10 · 4 минуты чтения

Срез хранит значения по номерам: 0, 1, 2… Часто удобнее находить значение по имени: цену — по названию товара, телефон — по фамилии. Для этого есть map (в других языках — словарь, ассоциативный массив, хеш-таблица): набор пар «ключ — значение», где каждый ключ встречается один раз.

Создание и основные действия

Тип map записывается как map[ключ]значение: map[string]int — ключи-строки, значения-числа.

Go
package main

import "fmt"

func main() {
    ages := map[string]int{"Анна": 20, "Борис": 31}
    ages["Вера"] = 25
    ages["Анна"] = 21
    fmt.Println(ages["Анна"], len(ages))

    delete(ages, "Борис")
    fmt.Println(ages)
    fmt.Println(ages["Глеб"])
}
Вывод
21 3
map[Анна:21 Вера:25]
0

fmt.Println печатает map с ключами по порядку — так задумано в пакете fmt, чтобы вывод был одинаковым. А вот при переборе в цикле порядок другой — об этом ниже.

Есть ли ключ: v, ok

Раз для отсутствующего ключа возвращается ноль, по одному значению не понять: ключа нет или там правда 0. Для проверки берут два значения:

Go
package main

import "fmt"

func main() {
    stock := map[string]int{"яблоки": 0, "груши": 12}

    if n, ok := stock["яблоки"]; ok {
        fmt.Println("яблоки есть в списке, осталось", n)
    }
    if _, ok := stock["сливы"]; !ok {
        fmt.Println("слив нет в списке")
    }
}
Вывод
яблоки есть в списке, осталось 0
слив нет в списке

Второе значение, ok, — true, если ключ есть. Запись if v, ok := m[k]; ok встречается в программах на Go постоянно.

stock ключ значение "яблоки" 0 "груши" 12 v ok stock["яблоки"] 0 true ключ есть stock["сливы"] 0 false ключа нет v в обоих случаях 0 — различает только ok
В stock у ключа «яблоки» значение 0, поэтому по одному v его не отличить от отсутствующего ключа «сливы» — v в обоих случаях 0, а ok равно true только для существующего ключа.

make и пустая map

Пустую map создают через make или пустым литералом {}:

Go
counts := make(map[string]int)
prices := map[string]float64{}

Просто объявить map нельзя: var m map[string]int даёт nil-map — читать из неё можно, а запись m["a"] = 1 вызывает панику assignment to entry in nil map. Подробно — в справке panic: assignment to entry in nil map.

var m map[string]int m nil таблицы нет чтение — можно, получите 0 m["a"] = 1 — паника assignment to entry in nil map counts := make(map[string]int) counts пустая таблица запись — можно или литерал {}
Объявленная через var map без make равна nil и таблицы не имеет: читать из неё можно, а запись даёт панику assignment to entry in nil map; make создаёт пустую таблицу, в которую можно писать.

Перебор и порядок ключей

range по map выдаёт пары «ключ — значение»:

Go
for name, age := range ages {
    fmt.Println(name, age)
}

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

Пример: подсчёт слов

Go
package main

import (
    "fmt"
    "slices"
    "strings"
)

func main() {
    text := "кот пёс кот рыба пёс кот"
    counts := make(map[string]int)
    for _, w := range strings.Fields(text) {
        counts[w]++
    }

    keys := make([]string, 0, len(counts))
    for k := range counts {
        keys = append(keys, k)
    }
    slices.Sort(keys)

    for _, k := range keys {
        fmt.Println(k, counts[k])
    }
}
Вывод
кот 3
пёс 2
рыба 1

counts[w]++ работает и для нового слова: значение отсутствующего ключа — 0, после ++ становится 1. Это обычный приём для подсчёта. for k := range counts берёт только ключи. Отсортированные ключи дают одинаковый вывод при каждом запуске.

text := "кот пёс кот рыба пёс кот" strings.Fields(text) кот пёс кот рыба пёс кот counts[w]++ counts рыба 1 кот 3 пёс 2 новое слово: 0 → 1 порядок случайный slices.Sort(keys) keys 0 кот 1 пёс 2 рыба по порядку, поэтому вывод одинаковый
strings.Fields делит текст на слова, counts считает каждое слово, начиная нового с 0, а ключи, собранные в срез keys и отсортированные slices.Sort, дают одинаковый вывод при каждом запуске.

Значения посложнее

Значением может быть что угодно: срез, структура, другая map.

Go
package main

import "fmt"

func main() {
    groups := make(map[string][]string)
    groups["Минск"] = append(groups["Минск"], "Анна")
    groups["Минск"] = append(groups["Минск"], "Борис")
    groups["Брест"] = append(groups["Брест"], "Вера")
    fmt.Println(groups["Минск"], len(groups["Минск"]))
    fmt.Println(groups["Гомель"] == nil)
}
Вывод
[Анна Борис] 2
true

Для отсутствующего ключа groups["Гомель"] возвращает nil-срез, а append к nil-срезу работает — поэтому отдельная проверка «есть ли уже такой город» не нужна.

Какие типы бывают ключами

Ключом может быть значение, которое сравнивается через ==: числа, строки, bool, массивы, структуры из таких полей. Срез, map и функция ключом быть не могут — программа не соберётся (invalid map key type).

Map передаётся по ссылке

Map, как и срез, при передаче в функцию не копируется: функция, которая меняет map, меняет ту же самую. Копию делают вручную в цикле или функцией maps.Clone из пакета maps.

Одну map нельзя менять из нескольких горутин одновременно без защиты: программа может упасть с ошибкой concurrent map writes. Как защищать общие данные — в уроке Горутины и каналы.

Коротко

Проверьте себя

1. Что вернёт m["x"] для m := map[string]int{}, если ключа "x" нет?
2. Как узнать, есть ли ключ в map?
3. В каком порядке for k, v := range m перебирает элементы?
4. Что случится при var m map[string]int и затем m["a"] = 1?