Язык Go — maps
Maps в Go — встроенная хеш-таблица. Разбираем синтаксис, 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
nilmap: читать безопасно, писать — паника. Всегда инициализируйте перед записью- Порядок итерации не гарантирован — для стабильного порядка сортируйте ключи