Maps - хеш-таблицы в Go

Maps (карты) в Go - это встроенный ассоциативный тип данных, который хранит пары ключ-значение. Думайте о них как об ассоциативных массивах PHP или объектах JavaScript.

Создание maps

Литерал map

// Пустая map
var m1 map[string]int // nil map, нельзя записывать!

// С инициализацией
scores := map[string]int{
    "Alice": 95,
    "Bob":   87,
    "Carol": 92,
}

// Пустая, но инициализированная
empty := map[string]int{}

Make

// Создание с make
ages := make(map[string]int)
ages["Alice"] = 30
ages["Bob"] = 25

// С начальной емкостью (подсказка для оптимизации)
large := make(map[string]int, 1000)
В nil map нельзя записывать, но можно безопасно читать (вернется нулевое значение).

Операции с maps

Запись и чтение

// Запись
m := make(map[string]string)
m["key"] = "value"

// Чтение
value := m["key"]

// Чтение с проверкой существования
value, exists := m["key"]
if exists {
    fmt.Println("Найдено:", value)
} else {
    fmt.Println("Ключ не найден")
}

Удаление

// Удаление по ключу
delete(m, "key")

// delete безопасен для несуществующих ключей
delete(m, "nonexistent") // не паникует

Итерация

scores := map[string]int{
    "Alice": 95,
    "Bob":   87,
}

// Порядок итерации случайный!
for name, score := range scores {
    fmt.Printf("%s: %d\n", name, score)
}

// Только ключи
for name := range scores {
    fmt.Println(name)
}

// Только значения
for _, score := range scores {
    fmt.Println(score)
}

Ключи и значения

Что может быть ключом?

Ключ должен быть сравнимым типом:

// Структура как ключ
type Point struct {
    X, Y int
}

grid := make(map[Point]string)
grid[Point{1, 2}] = "A"
grid[Point{3, 4}] = "B"
Слайсы, maps, функции не могут быть ключами, так как они не сравнимы.

Практические примеры

Подсчет частоты

func wordFrequency(text string) map[string]int {
    words := strings.Fields(text)
    freq := make(map[string]int)

    for _, word := range words {
        freq[word]++ // автоматически 0 + 1 для нового ключа
    }

    return freq
}

// Использование
text := "go is awesome go is fast"
freq := wordFrequency(text)
// map[awesome:1 fast:1 go:2 is:2]

Группировка

type Person struct {
    Name string
    Age  int
    City string
}

func groupByCity(people []Person) map[string][]Person {
    groups := make(map[string][]Person)

    for _, person := range people {
        groups[person.City] = append(groups[person.City], person)
    }

    return groups
}

Set (множество)

// В Go нет встроенного Set, используем map[T]bool
type Set map[string]bool

func NewSet(items ...string) Set {
    s := make(Set)
    for _, item := range items {
        s[item] = true
    }
    return s
}

func (s Set) Add(item string) {
    s[item] = true
}

func (s Set) Contains(item string) bool {
    return s[item]
}

func (s Set) Remove(item string) {
    delete(s, item)
}

// Или используем пустую структуру для экономии памяти
type IntSet map[int]struct{}

func (s IntSet) Add(n int) {
    s[n] = struct{}{}
}

Maps и конкурентность

Конкурентные чтение и запись в map приведут к панике. Используйте sync.Map или мьютексы (подробнее - в уроке про [горутины](./11-goroutines.md) и [каналы](./12-channels.md)).

С мьютексом

type SafeCounter struct {
    mu      sync.Mutex
    counter map[string]int
}

func (c *SafeCounter) Inc(key string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.counter[key]++
}

func (c *SafeCounter) Value(key string) int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.counter[key]
}

sync.Map

var m sync.Map

// Запись
m.Store("key", "value")

// Чтение
value, ok := m.Load("key")

// Удаление
m.Delete("key")

// Итерация
m.Range(func(key, value interface{}) bool {
    fmt.Printf("%v: %v\n", key, value)
    return true // продолжить итерацию
})

Паттерны и best practices

Lazy initialization

type Cache struct {
    data map[string]string
}

func (c *Cache) Get(key string) string {
    if c.data == nil {
        c.data = make(map[string]string)
    }
    return c.data[key]
}

Map of maps

// Двумерная таблица
table := make(map[string]map[string]int)

// Инициализация при необходимости
func setValue(table map[string]map[string]int, row, col string, value int) {
    if table[row] == nil {
        table[row] = make(map[string]int)
    }
    table[row][col] = value
}

Кеширование

type Fibonacci struct {
    cache map[int]int
}

func (f *Fibonacci) Get(n int) int {
    if f.cache == nil {
        f.cache = make(map[int]int)
    }

    if val, exists := f.cache[n]; exists {
        return val
    }

    var result int
    if n <= 1 {
        result = n
    } else {
        result = f.Get(n-1) + f.Get(n-2)
    }

    f.cache[n] = result
    return result
}

Производительность

<ComparisonTable data={{ headers: ["Операция", "Сложность", "Комментарий"], rows: [ ["Вставка", "O(1)", "Амортизированное"], ["Чтение", "O(1)", "В среднем"], ["Удаление", "O(1)", "В среднем"], ["Итерация", "O(n)", "n - количество элементов"], ["len()", "O(1)", "Просто чтение поля"] ] }} />

Советы по производительности

// Pre-size для больших maps
bigMap := make(map[string]int, 10000)

// Переиспользование maps
func process(m map[string]int) {
    // Очистка map для переиспользования
    for k := range m {
        delete(m, k)
    }
    // Теперь map пустая, но память выделена
}

// Struct{} вместо bool для sets
type Set map[string]struct{} // экономит память

Типичные ошибки

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

// ПАНИКА!
m := make(map[string]int)

go func() {
    for i := 0; i < 1000; i++ {
        m["key"] = i
    }
}()

go func() {
    for i := 0; i < 1000; i++ {
        _ = m["key"]
    }
}()

Изменение map во время итерации

// Безопасно добавлять
m := map[string]int{"a": 1, "b": 2}
for k, v := range m {
    m[k+"_new"] = v * 2 // OK
}

// Но порядок непредсказуем

Nil map

var m map[string]int
m["key"] = 1 // ПАНИКА!

// Правильно
m = make(map[string]int)
m["key"] = 1 // OK

JSON и maps

// Сериализация
data := map[string]interface{}{
    "name": "Alice",
    "age":  30,
    "tags": []string{"go", "backend"},
}

jsonBytes, _ := json.Marshal(data)

// Десериализация
var result map[string]interface{}
json.Unmarshal(jsonBytes, &result)

Итоги

  • Maps - основная структура для ассоциативных данных
  • Ключи должны быть сравнимыми
  • Помните про nil maps
  • Maps не потокобезопасны
  • Порядок итерации случайный

В следующем уроке изучим структуры - пользовательские типы данных!

Типичная ошибка

Считать, что отсутствующий ключ - это ошибка. На самом деле вернётся «нулевое значение» - поэтому и нужен ok.

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.