Конкуренция в Go
Теория: sync.WaitGroup
Когда в программе запускается несколько горутин, у каждой свой жизненный цикл. Они могут работать дольше, чем main, ждать ввод-вывод, обрабатывать данные. Если главная горутина завершится раньше, рантайм мгновенно остановит весь процесс — даже если часть работы еще не закончена.
sync.WaitGroup решает ровно эту задачу: позволяет дождаться завершения группы горутин. Он не занимается блокировкой доступа к данным и не защищает память. Единственная его ответственность — учет количества активных задач и ожидание, пока все они отработают до конца.
Зачем нужен WaitGroup
Без WaitGroup() остается грубый способ — ставить time.Sleep в надежде, что «за это время все успеет выполниться». Такой подход не масштабируется: одна горутина может отработать за миллисекунду, другая — за секунду, третья упереться в внешний сервис. В итоге либо программа завершается слишком рано, либо делает бессмысленные паузы.
WaitGroup() заменяет угадывание конкретным протоколом. У него есть внутренний счетчик активных горутин.
- Перед запуском горутины счетчик увеличивается.
- Когда горутина заканчивает работу, счетчик уменьшается.
- В момент, когда счетчик станет равен нулю, ожидание можно считать завершенным.
Главная горутина вызывает wg.Wait() и блокируется до тех пор, пока все запущенные задачи не отметятся через Done(). Это гарантирует, что программа не оборвется, пока есть незаконченная работа.
Простейший пример:
Здесь main не продолжит выполнение, пока три worker не отработают до конца. Сколько бы времени ни заняла каждая из них, WaitGroup() гарантирует корректный момент выхода.
Методы Add, Done, Wait
Поведение WaitGroup() целиком определяется тремя методами:
Add(delta int)— изменяет счетчик ожидаемых горутин;Done()— сокращенная формаAdd(-1);Wait()— блокирует горутину до тех пор, пока счетчик не станет нулем.
Последовательность действий всегда одна и та же:
- Перед запуском горутины вызвать
Add(1). - Внутри горутины как можно раньше сделать
defer wg.Done(). - После запуска всех горутин один раз вызвать
wg.Wait()там, где нужно дождаться их завершения.
Разбор на небольшом примере:
Тонкий момент: Add() и Done() могут вызываться из разных горутин, но протокол должен оставаться согласованным. Add() увеличивает ожидаемое количество задач, Done() снижает его по факту выполнения, Wait() фиксирует точку, в которой нужно дождаться, пока счетчик обнулится.
Типичные ошибки использования WaitGroup
Интерфейс WaitGroup() простой, но ошибки все равно встречаются регулярно. Почти всегда проблема связана с неправильным управлением счетчиком.
1. Add вызывается внутри горутины
Неправильно:
На первый взгляд кажется, что все нормально: Add(1) вызывается до Done(), и defer не может сработать раньше. Но реальная проблема в другом.
Горутина может вообще не успеть запуститься.
Запуск через go — это только планирование. Основная горутина может мгновенно дойти до wg.Wait() и увидеть счетчик равным нулю. Она перестает ждать и выходит из функции, а рабочая горутина еще даже не начала выполняться — и ее Add(1) вызывается после Wait().
Это гонка между Add и Wait. Такое использование WaitGroup() считается некорректным, и поведение программы становится неопределенным.
Правильно:
Здесь порядок гарантирован: сначала увеличили счетчик, потом запустили горутину, и Wait() точно дождется ее выполнения.
3. Параллельный Add и Wait
Спецификация позволяет вызывать Add() и Wait() конкурентно, но только если гарантируется, что Add() не увеличивает счетчик после того, как Wait() уже решило, что можно выйти. На практике это легко нарушить.
Безопасный шаблон проще:
- все
Add()выполняются в одном месте перед запуском горутин; Wait()вызывается после этого и больше не пересекается по времени сAdd().
Это устраняет класс проблем, когда одна часть программы еще добавляет работу, а другая уже решила, что все окончено.
4. Отсутствие Done при раннем выходе
Если внутри горутины есть несколько веток выхода, а Done() вызывается не через defer, одна из веток легко забудет уменьшить счетчик. В результате Wait() никогда не дождется нуля и повиснет.
Надежный шаблон:
5. Смешивание WaitGroup() с закрытием каналов
Частый прием — закрывать канал после того, как все горутины-писатели завершились. Здесь важно не перепутать роли:
- горутины только пишут в канал и вызывают
Done(); - одна отдельная горутина или
mainждетwg.Wait()и только после этого закрывает канал.
Если закрыть канал раньше, чем все писатели закончат работу, при первой попытке записи произойдет panic: send on closed channel.
sync.WaitGroup — это счетчик жизни горутин. Он не отвечает за то, как они взаимодействуют между собой, не синхронизирует доступ к данным, не решает проблемы гонок. Его роль узкая и очень полезная: точно знать, когда все конкурентные задачи действительно завершились.
Как только в коде появляется go, рядом почти всегда должен появиться либо WaitGroup(), либо контекст и сигнальные каналы. Это признак управляемой конкуренции: каждая горутина не только запускается, но и имеет понятный момент, когда ее ждут и отпускают.
И еще одна ошибка — работа с копией wg.
Если воркер будет, например, так, то Done() будет вызываться у копии и Wait() для исходной переменной никогда не достигнет нуля:


