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

Найти баг: WaitGroup и горутины

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

Условие

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

func FindMaxProblem() {
    var maxNum int
    for i := 1000; i > 0; i-- {
        go func() {
            if i%2 == 0 && i > maxNum {
                maxNum = i
            }
        }()
    }
    fmt.Printf("Maximum is %d", maxNum)
}

Вопросы

  1. Какие проблемы есть в этом коде?
  2. Почему результат будет неожиданным?
  3. Напишите исправленную версию

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

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

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

Решение

Это задача с множественными проблемами: race condition, closure-захват переменной и race condition при доступе к переменной. Демонстрирует типичные ошибки с concurrency в Go.

1. Какие проблемы в коде?

Проблема 1: Closure захватывает переменную цикла

for i := 1000; i > 0; i-- {
    go func() {
        if i%2 == 0 ...  // захватывает переменную i
    }()
}

Все горутины захватывают одну переменную i, которая меняется.

Проблема 2: Data Race на переменной maxNum

var maxNum int
// Несколько горутин одновременно читают и пишут в maxNum БЕЗ синхронизации
if i > maxNum {      // race: чтение
    maxNum = i       // race: запись
}

Проблема 3: Отсутствует WaitGroup

for i := 1000; i > 0; i-- {
    go func() { ... }()
}
fmt.Printf("Maximum is %d", maxNum)  // главная горутина не ждёт!

Главная горутина выводит результат, прежде чем все background горутины завершатся.

2. Почему результат неожиданный?

=== Возможный вывод ===
Maximum is 0

=== Почему ===
1. Все 1000 горутин создаются со ссылкой на переменную i
2. i уменьшается от 1000 до -1 в цикле
3. Main горутина выполняет fmt.Printf ДО того, как background горутины начнут работать
4. maxNum остаётся 0 (значение по умолчанию)
5. Background горутины начинают работать, но main уже вывел результат

=== Или ещё случай ===
Компилятор с флагом -race обнаружит data race:
WARNING: DATA RACE
Write at ... by goroutine 10
Previous write at ... by goroutine 9

3. Исправленная версия

Вариант 1: Использовать WaitGroup + передать параметр

func FindMaxFixed() {
    var wg sync.WaitGroup
    var mu sync.Mutex
    var maxNum int
    
    for i := 1000; i > 0; i-- {
        wg.Add(1)
        go func(num int) {  // ← передаём параметр
            defer wg.Done()
            
            if num%2 == 0 {
                mu.Lock()  // ← синхронизируем доступ к maxNum
                if num > maxNum {
                    maxNum = num
                }
                mu.Unlock()
            }
        }(i)  // ← передаём значение i
    }
    
    wg.Wait()  // ← ждём завершения всех горутин
    fmt.Printf("Maximum is %d\n", maxNum)
}

Вариант 2: Локальная переменная в цикле

func FindMaxFixed2() {
    var wg sync.WaitGroup
    var mu sync.Mutex
    var maxNum int
    
    for i := 1000; i > 0; i-- {
        i := i  // ← создаём локальную копию для каждой итерации
        wg.Add(1)
        go func() {
            defer wg.Done()
            
            if i%2 == 0 {
                mu.Lock()
                if i > maxNum {
                    maxNum = i
                }
                mu.Unlock()
            }
        }()
    }
    
    wg.Wait()
    fmt.Printf("Maximum is %d\n", maxNum)
}

Вариант 3: Использовать sync.Map вместо mutex

func FindMaxFixed3() {
    var wg sync.WaitGroup
    var results sync.Map
    
    for i := 1000; i > 0; i-- {
        wg.Add(1)
        go func(num int) {
            defer wg.Done()
            if num%2 == 0 {
                results.Store(num, true)
            }
        }(i)
    }
    
    wg.Wait()
    
    maxNum := 0
    results.Range(func(key, value interface{}) bool {
        if num := key.(int); num > maxNum {
            maxNum = num
        }
        return true
    })
    
    fmt.Printf("Maximum is %d\n", maxNum)
}

Вариант 4: Использовать каналы (most idiomatic)

func FindMaxFixed4() {
    results := make(chan int)
    
    go func() {
        for i := 1000; i > 0; i-- {
            i := i
            go func() {
                if i%2 == 0 {
                    results <- i
                }
            }()
        }
    }()
    
    maxNum := 0
    received := 0
    
    // Собираем результаты
    for num := range results {
        if num > maxNum {
            maxNum = num
        }
        received++
        if received == 500 {  // ожидаемое количество чётных чисел
            close(results)
            break
        }
    }
    
    fmt.Printf("Maximum is %d\n", maxNum)
}

Пошаговое сравнение

Оригинальный (неправильный) код:

1. for i := 1000; i > 0; i-- (цикл с i от 1000 до 1)
   ├─ create goroutine 1 (захватывает ссылку на i)
   ├─ create goroutine 2 (захватывает ссылку на i)
   ├─ ...
   └─ create goroutine 1000 (захватывает ссылку на i)
   
2. цикл завершён: i = 0

3. fmt.Printf выводит maxNum = 0  ← СЛИШКОМ БЫСТРО!
   Горутины ещё не запустились!

4. Background горутины начинают работать:
   goroutine 1: if 0%2 == 0 ...  ← i уже 0!
   goroutine 2: if 0%2 == 0 ...
   ...
   
5. Все видят i = 0 → maxNum не меняется

Исправленный код (вариант 1):

1. for i := 1000; i > 0; i-- {
   ├─ wg.Add(1)
   ├─ go func(num int) {...}(i)  ← передаём значение
   └─ горутина получает свою копию i

2. wg.Wait()  ← ждём завершения всех 1000 горутин

3. Горутины работают параллельно:
   goroutine 1: if 998%2 == 0 && 998 > maxNum → maxNum = 998
   goroutine 2: if 996%2 == 0 && 996 > maxNum → lock → maxNum = 996 (не выполнится)
   ...
   goroutine 500: if 1000%2 == 0 && 1000 > maxNum → maxNum = 1000

4. После wg.Wait() все горутины завершены

5. fmt.Printf выводит maxNum = 1000  ← ПРАВИЛЬНО!

Полный исправленный код с объяснениями

package main

import (
    "fmt"
    "sync"
)

// Вариант 1: с WaitGroup и mutex (РЕКОМЕНДУЕТСЯ)
func FindMaxCorrect() {
    var wg sync.WaitGroup
    var mu sync.Mutex  // защищает maxNum от data race
    var maxNum int
    
    // Используем буферизованный канал для отправки чисел
    numbers := make(chan int, 100)
    
    // Отправляем числа
    go func() {
        for i := 1000; i > 0; i-- {
            numbers <- i
        }
        close(numbers)
    }()
    
    // Обрабатываем в горутинах
    for num := range numbers {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            
            if n%2 == 0 {
                mu.Lock()
                if n > maxNum {
                    maxNum = n
                }
                mu.Unlock()
            }
        }(num)
    }
    
    wg.Wait()
    fmt.Printf("Maximum even number is %d\n", maxNum)
}

func main() {
    FindMaxCorrect()
    // Output: Maximum even number is 1000
}

Итоговая таблица проблем и решений

ПроблемаПричинаРешение
Closure захватывает iСсылка на переменную циклаПередать как параметр или i := i
Data race на maxNumОдновременное чтение/записьsync.Mutex
Результат выводится слишком раноНет ожидания горутинsync.WaitGroup + Wait()

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

  1. Всегда используй WaitGroup для синхронизации горутин
  2. Передавай переменные цикла как параметры или используй i := i
  3. Защищай общие переменные Mutex-ом от data race
  4. Запускай с флагом -race для обнаружения race conditions
  5. Каналы часто более идиоматичны чем mutex в Go

Эта задача демонстрирует три самых частых ошибки в concurrent Go коде, которые спрашивают на собеседованиях.