← Назад к вопросам

Найти баг: Data Race в map

2.3 Middle🔥 221 комментариев
#Конкурентность и горутины#Основы Go

Условие

Найдите проблему в следующем коде и исправьте её:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    m := make(map[int]int)

    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            m[i] = i * i
        }(i)
    }

    wg.Wait()
    fmt.Println(m)
}

Вопросы

  1. Какая проблема в этом коде?
  2. Как её исправить? (приведите исправленный код)
  3. Какие есть альтернативные способы решения?

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение

Это классическая задача на обнаружение data race. Go имеет встроенный инструмент для обнаружения именно таких проблем.

Ответ на вопросы

1. Какая проблема в этом коде?

Проблема: Data Race (race condition)

Мапа в Go НЕ потокобезопасна. Несколько горутин одновременно пишут в одну и ту же map без синхронизации.

Горотина 1: m[10] = 100
Горотина 2: m[20] = 400  ← одновременно!
Горотина 3: m[30] = 900
Горотина 4: m[40] = 1600

Внутри Go это вызывает проблемы:

  • Потеря данных (некоторые записи теряются)
  • Panic (может быть рассогласование внутренних структур map)
  • Undefined behavior (непредсказуемые результаты)

Как обнаружить data race?

Запустить с флагом -race:

go run -race main.go
# ИЛИ
go test -race ./...

Вывод:

==================
WARNING: DATA RACE
Write at 0x00c0001da000 by goroutine 8:
    main.main.func1()
        /path/to/main.go:15 +0x44

Previous write at 0x00c0001da000 by goroutine 7:
    main.main.func1()
        /path/to/main.go:15 +0x44

Goroutine 7 (running) created at:
    main.main()
        /path/to/main.go:12 +0xb8
Goroutine 8 (running) created at:
    main.main()
        /path/to/main.go:12 +0xb8
==================
Found 1 data race(s)
fatal error: races detected

2. Как исправить?

Вариант 1: Использовать sync.Mutex (рекомендуется для стандартной map)

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    var mu sync.Mutex  // ← добавляем мьютекс
    m := make(map[int]int)

    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            mu.Lock()        // ← блокируем доступ
            m[i] = i * i
            mu.Unlock()      // ← разблокируем
        }(i)
    }

    wg.Wait()
    fmt.Println(m)
}

Вариант 2: Использовать sync.RWMutex (если много читаний)

var mu sync.RWMutex  // RWMutex для читателей/писателей

// Для записи
mu.Lock()
m[i] = i * i
mu.Unlock()

// Для чтения
mu.RLock()
val := m[i]
mu.RUnlock()

Вариант 3: Обработать в одной горутине (простейший вариант для этого кода)

func main() {
    var wg sync.WaitGroup
    m := make(map[int]int)
    ch := make(chan [2]int, 100)  // канал для результатов

    // Горутины отправляют результаты в канал
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            ch <- [2]int{i, i * i}  // отправляем пару
        }(i)
    }

    // Одна горутина читает из канала и пишет в map
    go func() {
        for j := 0; j < 100; j++ {
            pair := <-ch
            m[pair[0]] = pair[1]  // безопасная запись
        }
    }()

    wg.Wait()
    fmt.Println(m)
}

Вариант 4: Использовать sync.Map (встроенное решение)

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    m := &sync.Map{}  // ← потокобезопасная map

    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            m.Store(i, i*i)  // потокобезопасное добавление
        }(i)
    }

    wg.Wait()
    
    // Итерируем по sync.Map
    m.Range(func(key, value interface{}) bool {
        fmt.Printf("%v: %v\n", key, value)
        return true
    })
}

Сравнение подходов

ВариантСпособПлюсыМинусы
MutexLock/Unlockявный контрольможно забыть unlock
defer + Mutexdefer вызывает unlockбезопаснеенемного медленнее
Каналгорутина читает результатыno raceесли много горутин может быть узкое место
sync.Mapвстроенная потокобезопасностьпростая APIмедленнее обычной map, нет Range в старых версиях

Рекомендуемый исправленный код

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    var mu sync.Mutex
    m := make(map[int]int)

    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            mu.Lock()
            defer mu.Unlock()  // ← гарантирует unlock даже при panic
            m[i] = i * i
        }(i)
    }

    wg.Wait()
    
    // Безопасное читение
    mu.Lock()
    fmt.Println(m)
    mu.Unlock()
}

3. Альтернативные способы решения

Способ 1: sync.Map (встроенное решение)

func main() {
    var wg sync.WaitGroup
    m := &sync.Map{}

    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            m.Store(i, i*i)
        }(i)
    }

    wg.Wait()
}

Когда использовать:

  • Много одновременных читаний
  • Ключи стабильны (редко изменяются)
  • Простой случай использования

Способ 2: Разделённые maps (sharding)

Для больших систем — распределить нагрузку на несколько map:

const numShards = 10

type ShardedMap struct {
    shards [numShards]struct {
        sync.RWMutex
        m map[int]int
    }
}

func (sm *ShardedMap) Set(key, value int) {
    shard := &sm.shards[key%numShards]
    shard.Lock()
    shard.m[key] = value
    shard.Unlock()
}

// Уменьшает contention (конкурирование за мьютекс)

Способ 3: Channels + Worker pool

func main() {
    var wg sync.WaitGroup
    m := make(map[int]int)
    jobs := make(chan [2]int, 100)

    // 4 воркера читают из канала и пишут в map
    for w := 0; w < 4; w++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for job := range jobs {
                m[job[0]] = job[1]  // один доступ в раз
            }
        }()
    }

    // Отправляем работу
    for i := 0; i < 100; i++ {
        jobs <- [2]int{i, i * i}
    }
    close(jobs)

    wg.Wait()
    fmt.Println(m)
}

Способ 4: Contexted RWMutex

type SafeMap struct {
    mu    sync.RWMutex
    items map[int]int
}

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

func (sm *SafeMap) Get(key int) (int, bool) {
    sm.mu.RLock()
    defer sm.mu.RUnlock()
    val, ok := sm.items[key]
    return val, ok
}

func main() {
    sm := &SafeMap{items: make(map[int]int)}
    // ...
    sm.Set(1, 10)
    val, _ := sm.Get(1)
}

Ключевой вывод

**Maps в Go не потокобезопасны. Для параллельного доступа используйте:

  1. sync.Mutex — для простых случаев (рекомендуется)
  2. sync.RWMutex — если много читаний
  3. sync.Map — встроенное решение
  4. Каналы — для передачи данных между горутинами
  5. Sharding — для высоконагруженных систем**

Всегда запускайте с флагом -race в тестах и разработке!

Найти баг: Data Race в map | PrepBro