Итерация по 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-м запуске.
Как избежать?
Есть три проверенных способа:
- 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
})
}
- 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
}
- 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? Ответ: