Итерация по map в Go: 5 фатальных ошибок и как их избежать

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Итерация по map в Go: как не наступить на грабли (и не потерять данные)

Если вы когда-нибудь писали на Go, то наверняка сталкивались с map — одной из самых мощных и коварных структур данных. Казалось бы, что может быть проще: взял range, прошелся по ключам, получил значения. Но дьявол, как всегда, в деталях. Вопрос "Iterating over all the keys of a map" на Stack Overflow собрал тысячи просмотров, и это не случайно.

Сегодня мы разберем топ ошибок при итерации по map в Go, которые допускают даже опытные разработчики. Вы узнаете, почему порядок обхода — это лотерея, как безопасно работать с map в горутинах и что делать, если map вдруг оказался nil. Поехали!

Ошибка №1: Надежда на порядок (или "Где мои ключи?")

Самая распространенная ловушка — уверенность, что range вернет ключи в том же порядке, в котором вы их добавляли. В Go это не так. Порядок итерации по map принципиально недетерминирован.

Посмотрите на этот код:

package main

import "fmt"

func main() {
    m := map[string]int{
        "apple":  1,
        "banana": 2,
        "cherry": 3,
    }

    for i := 0; i < 3; i++ {
        fmt.Println("Итерация", i+1)
        for k, v := range m {
            fmt.Printf("  %s: %d\n", k, v)
        }
    }
}

Скорее всего, каждый запуск выдаст разный порядок. Это не баг, а фича: Go специально рандомизирует порядок, чтобы разработчики не привыкали к нему и не строили логику на случайной последовательности.

Как избежать?

  • Не полагайтесь на порядок — это основное правило.
  • Если порядок важен, используйте []string для хранения ключей отдельно и сортируйте их:
package main

import (
    "fmt"
    "sort"
)

func main() {
    m := map[string]int{"apple": 1, "banana": 2, "cherry": 3}

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

    for _, k := range keys {
        fmt.Printf("%s: %d\n", k, m[k])
    }
}

Совет: Если вам нужно сохранить порядок вставки, рассмотрите библиотеку github.com/elliotchance/orderedmap или напишите свою обертку на основе двух структур (map + slice).

Ошибка №2: Итерация с одновременной записью (data race)

В Go map небезопасен для конкурентного доступа. Если одна горутина читает (итерирует) map, а другая в это же время пишет в нее, вы получите fatal error: concurrent map iteration and map write.

Пример фатальной ошибки:

package main

import (
    "fmt"
    "time"
)

func main() {
    m := make(map[int]int)

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

    go func() {
        for k, v := range m {
            fmt.Println(k, v)
        }
    }()

    time.Sleep(time.Second)
}

Этот код упадет с паникой. И не всегда — race condition может проявиться на 1000-м запуске.

Как избежать?

Есть три проверенных способа:

  1. sync.Mutex — классический мьютекс для синхронизации доступа:
package main

import (
    "fmt"
    "sync"
)

type SafeMap struct {
    mu sync.Mutex
    m  map[int]int
}

func (s *SafeMap) Set(key, value int) {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.m[key] = value
}

func (s *SafeMap) Iterate(f func(key, value int) bool) {
    s.mu.Lock()
    defer s.mu.Unlock()
    for k, v := range s.m {
        if !f(k, v) {
            break
        }
    }
}

func main() {
    sm := SafeMap{m: make(map[int]int)}
    sm.Set(1, 10)
    sm.Set(2, 20)

    sm.Iterate(func(k, v int) bool {
        fmt.Println(k, v)
        return true
    })
}
  1. sync.RWMutex — если чтений гораздо больше, чем записей:
type RWSafeMap struct {
    mu sync.RWMutex
    m  map[int]int
}

func (s *RWSafeMap) Get(key int) int {
    s.mu.RLock()
    defer s.mu.RUnlock()
    return s.m[key]
}

func (s *RWSafeMap) Set(key, value int) {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.m[key] = value
}
  1. sync.Map — встроенная конкурентно-безопасная map (Go 1.9+). Идеально для кэшей и счетчиков:
package main

import (
    "fmt"
    "sync"
)

func main() {
    var m sync.Map

    // Запись
    m.Store("hello", "world")

    // Чтение
    if val, ok := m.Load("hello"); ok {
        fmt.Println(val) // world
    }

    // Итерация
    m.Range(func(key, value interface{}) bool {
        fmt.Println(key, value)
        return true // продолжаем
    })
}

Важно: sync.Map не подходит для всех сценариев. Для частых записей используйте обычный map + мьютекс.

Ошибка №3: Итерация по nil-map (и почему это работает)

Удивительно, но итерация по nil-map не вызывает панику — цикл просто не выполнится ни разу. Однако запись в nil-map приведет к panic: assignment to entry in nil map.

Пример:

var m map[string]int // nil map

for k, v := range m {
    fmt.Println(k, v) // никогда не выполнится
}

m["key"] = 1 // panic!

Как избежать?

  • Всегда инициализируйте map через make() или литералом: m := make(map[string]int).
  • Перед записью проверяйте, не nil ли map:
if m == nil {
    m = make(map[string]int)
}
m["key"] = 1

Лайфхак: В Go 1.21 появилась функция maps.Copy, которая безопасно копирует map, но она тоже требует инициализированного получателя.

Ошибка №4: Модификация map во время итерации

Можно ли удалять или добавлять элементы в map во время range? Ответ:

Блоги

Рекомендаций по книгам