Найти баг: WaitGroup и горутины
Условие
Найдите все проблемы в следующем коде:
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)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение
Это задача с множественными проблемами: 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() |
Ключевые выводы
- Всегда используй WaitGroup для синхронизации горутин
- Передавай переменные цикла как параметры или используй i := i
- Защищай общие переменные Mutex-ом от data race
- Запускай с флагом -race для обнаружения race conditions
- Каналы часто более идиоматичны чем mutex в Go
Эта задача демонстрирует три самых частых ошибки в concurrent Go коде, которые спрашивают на собеседованиях.