Найти баг: Data Race в map
Условие
Найдите проблему в следующем коде и исправьте её:
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)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение
Это классическая задача на обнаружение 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
})
}
Сравнение подходов
| Вариант | Способ | Плюсы | Минусы |
|---|---|---|---|
| Mutex | Lock/Unlock | явный контроль | можно забыть unlock |
| defer + Mutex | defer вызывает 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 не потокобезопасны. Для параллельного доступа используйте:
- sync.Mutex — для простых случаев (рекомендуется)
- sync.RWMutex — если много читаний
- sync.Map — встроенное решение
- Каналы — для передачи данных между горутинами
- Sharding — для высоконагруженных систем**
Всегда запускайте с флагом -race в тестах и разработке!