· 9 мин 👁 1.2k Начинающий

Язык Go — maps

Maps в Go — встроенная хеш-таблица. Разбираем синтаксис, comma ok идиому, безопасное удаление, допустимые типы ключей и неочевидные ситуации.

mapsхеш-таблицаключиcomma ok
Содержание

Map — встроенная хеш-таблица в Go. Ключи одного типа, значения другого. Синтаксис простой, но несколько деталей поведения стоит знать заранее.

Создание и инициализация

Map создаётся через составной литерал или через make:

// составной литерал — удобно при известных данных
var timeZone = map[string]int{
    "UTC":  0 * 60 * 60,
    "EST": -5 * 60 * 60,
    "CST": -6 * 60 * 60,
    "MST": -7 * 60 * 60,
    "PST": -8 * 60 * 60,
}

// make — когда данные появятся позже
counts := make(map[string]int)

Пустой map через литерал и через make эквивалентны:

a := map[string]int{}
b := make(map[string]int)
// a и b работают одинаково

Чтение, запись, удаление

Синтаксис как у массивов — только индекс не обязательно целое число:

offset := timeZone["EST"] // -18000

timeZone["NST"] = -3*60*60 - 30*60 // добавляем Ньюфаундленд, UTC-3:30

delete(timeZone, "PDT") // удаляем — безопасно, даже если ключа нет
delete(timeZone, "PDT") // повторное удаление — тоже не паника

Нулевое значение для отсутствующего ключа

Если ключа нет — map возвращает нулевое значение для типа значений. Для int это 0, для string"", для указателя — nil:

fmt.Println(timeZone["XYZ"]) // 0 — ключа нет, но паники нет

// нулевое значение можно использовать напрямую для счётчиков:
words := []string{"go", "is", "go", "go", "fun"}
freq := make(map[string]int)
for _, w := range words {
    freq[w]++ // если ключа нет, берётся 0, прибавляется 1
}
fmt.Println(freq) // map[fun:1 go:3 is:1]

Map как множество (set)

Map с типом значения bool работает как множество:

attended := map[string]bool{
    "Ann": true,
    "Joe": true,
}

person := "Ann"
if attended[person] { // false если person нет в map
    fmt.Println(person, "was at the meeting")
}

// проверка принадлежности — читается как обращение к множеству
fmt.Println(attended["Bob"]) // false — не было на встрече

Ещё один типичный паттерн — дедупликация:

words := []string{"go", "rust", "go", "python", "rust", "go"}

unique := make(map[string]bool)
for _, w := range words {
    unique[w] = true
}

fmt.Println(len(unique)) // 3
for w := range unique {
    fmt.Println(w) // порядок не гарантирован
}

Различить нулевое значение от отсутствия ключа

Нулевое значение и отсутствие ключа неразличимы при простом чтении. UTC имеет смещение 0 — это реальное значение, а не признак того, что ключа нет. Для различения — идиома «comma ok»:

var seconds int
var ok bool
seconds, ok = timeZone["UTC"]
// seconds == 0, ok == true  — ключ есть, значение 0

seconds, ok = timeZone["XYZ"]
// seconds == 0, ok == false — ключа нет

Типичное использование — внутри if с коротким объявлением:

func offset(tz string) int {
    if seconds, ok := timeZone[tz]; ok {
        return seconds // ключ найден
    }
    log.Println("unknown time zone:", tz)
    return 0
}

Если значение не нужно, только факт наличия — используем _:

_, present := timeZone["EST"]
fmt.Println(present) // true

_, present = timeZone["XYZ"]
fmt.Println(present) // false

Какие типы можно использовать как ключи

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

// структура как ключ — работает, если все поля сравнимы
type Point struct{ X, Y int }

distances := map[Point]float64{
    {0, 0}: 0,
    {3, 4}: 5, // расстояние от начала координат
}
fmt.Println(distances[Point{3, 4}]) // 5

// массив как ключ — работает
visited := map[[2]int]bool{}
visited[[2]int{1, 2}] = true
fmt.Println(visited[[2]int{1, 2}]) // true

// слайс как ключ — ошибка компиляции:
// bad := map[[]int]bool{}  // invalid map key type []int

Структуры с несравнимыми полями (слайсами, картами) тоже нельзя использовать как ключи:

type Bad struct {
    Tags []string // слайс — несравнимое поле
}
// bad := map[Bad]int{} // ошибка компиляции

Map — ссылочный тип

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

func addEntry(m map[string]int, key string, val int) {
    m[key] = val // изменяет оригинальную map
}

m := map[string]int{"a": 1}
addEntry(m, "b", 2)
fmt.Println(m) // map[a:1 b:2]

При этом переприсвоение самой переменной внутри функции не влияет на вызывающий код — передаётся копия указателя, а не указатель на указатель:

func tryReplace(m map[string]int) {
    m = map[string]int{"x": 99} // заменяем локальную копию ссылки
}

m := map[string]int{"a": 1}
tryReplace(m)
fmt.Println(m) // map[a:1] — оригинал не изменился

nil map: читать можно, писать нельзя

nil map ведёт себя как пустая при чтении, но паникует при записи:

var m map[string]int // m == nil

fmt.Println(m["key"]) // 0 — чтение безопасно
fmt.Println(len(m))   // 0 — безопасно

_, ok := m["key"]
fmt.Println(ok) // false — безопасно

// m["key"] = 1 // panic: assignment to entry in nil map

Всегда инициализируйте map перед записью:

var m map[string]int
m = make(map[string]int) // теперь можно писать
m["key"] = 1

Итерация по map

range по map возвращает пары ключ-значение. Порядок не гарантирован и меняется между запусками:

m := map[string]int{"a": 1, "b": 2, "c": 3}

for k, v := range m {
    fmt.Println(k, v) // порядок непредсказуем
}

// только ключи
for k := range m {
    fmt.Println(k)
}

Если нужен стабильный порядок — собираем ключи в слайс и сортируем:

import "sort"

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

for _, k := range keys {
    fmt.Println(k, m[k]) // теперь порядок алфавитный
}

Удаление во время итерации

В Go это явно разрешено и безопасно — в отличие от многих других языков:

m := map[string]int{"a": 1, "b": -2, "c": 3, "d": -4}

// удаляем все отрицательные значения прямо во время обхода
for k, v := range m {
    if v < 0 {
        delete(m, k)
    }
}
fmt.Println(m) // map[a:1 c:3]

Вложенные maps

Map может содержать другие maps в качестве значений:

// граф: смежность вершин с весами рёбер
graph := map[string]map[string]int{
    "A": {"B": 1, "C": 4},
    "B": {"C": 2, "D": 5},
    "C": {"D": 1},
}

// безопасное чтение из вложенной map
if neighbors, ok := graph["A"]; ok {
    fmt.Println("соседи A:", neighbors) // map[B:1 C:4]
}

// добавление ребра требует инициализации внутренней map
graph["D"] = make(map[string]int) // иначе паника при записи
graph["D"]["A"] = 7

Итоги

  • Map — ссылочный тип. Передача в функцию не копирует данные
  • Чтение несуществующего ключа возвращает нулевое значение — паники нет
  • Чтобы различить ноль от отсутствия — используйте идиому «comma ok»: v, ok := m[key]
  • delete(m, key) безопасен, даже если ключа нет
  • Удалять из map во время range — допустимо и безопасно
  • Ключами могут быть числа, строки, массивы, структуры — но не слайсы и не maps
  • nil map: читать безопасно, писать — паника. Всегда инициализируйте перед записью
  • Порядок итерации не гарантирован — для стабильного порядка сортируйте ключи