Menu

WaitGroup в Golang: Add, Done, Wait и пул воркеров

Как sync.WaitGroup ждёт завершения группы горутин: правила Add, Done и Wait, почему его передают по указателю, сбор результатов и ошибок и пул воркеров на его основе.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Базовый шаблон

sync.WaitGroup считает работающие горутины. Add увеличивает счётчик, Done уменьшает, Wait блокируется, пока он не станет нулём.

Три загрузки выполняются конкурентно, поэтому программа занимает около 10 мс вместо 30. Результаты печатаются в порядке ввода, потому что каждая горутина пишет только в свой индекс sizes, а main читает их только после Wait.

Нулевое значение WaitGroup готово к использованию. Конструктор не нужен.

Три правила

Вызывайте Add до go, а не внутри горутины. Если горутина сама вызывает Add, main может дойти до Wait раньше, чем стартует хоть одна горутина, увидеть ноль и вернуться, когда работа ещё не началась. Если количество известно заранее, один вызов wg.Add(len(files)) перед циклом равнозначен.

Вызывайте Done через defer первой строкой горутины. Горутина, которая досрочно возвращается из-за ошибки или паникует, всё равно уменьшит счётчик. Пропущенный Done оставляет Wait заблокированным навсегда. Если это последняя оставшаяся горутина, среда выполнения сообщит fatal error: all goroutines are asleep с sync.WaitGroup.Wait в трассировке.

Никогда не копируйте WaitGroup после первого использования. Передавайте в функции *sync.WaitGroup или захватывайте переменную в замыкании, как выше.

Передача WaitGroup в функцию

Когда тело горутины это именованная функция, передавайте указатель:

Если бы wg sync.WaitGroup был параметром-значением, каждый воркер вызывал бы Done на своей копии, а main навсегда заблокировался бы в Wait. go vet ловит это ещё до запуска:

./main.go:8:24: worker passes lock by value: sync.WaitGroup contains sync.noCopy

Более чистый дизайн вообще убирает конкурентность из worker: пусть это будет обычная функция, а учёт Add/Done делается в замыкании вызывающего кода. Тогда worker легко тестировать и вызывать синхронно.

Отрицательный счётчик

Done это Add(-1). Если счётчик опускается ниже нуля, программа паникует:

Вывод: recovered: sync: negative WaitGroup counter. Обычная причина: горутина с defer wg.Done(), которая на каком-то пути ещё и явно вызывает wg.Done().

Сбор ошибок

WaitGroup только считает. Для ошибок дайте каждой горутине свою ячейку и проверьте их после Wait:

errors.Join (Go 1.20) пропускает значения nil и возвращает nil, если все они nil, так что он объединяет «по одной ошибке на горутину» без дополнительного учёта.

Если нужно остановить оставшуюся работу, как только одна горутина завершилась с ошибкой, используйте golang.org/x/sync/errgroup. Это WaitGroup плюс первая ошибка плюс контекст, который отменяется при сбое, а g.SetLimit(n) ограничивает конкурентность. Пакет не входит в стандартную библиотеку, поэтому в редакторе на этой странице его не запустить:

g, ctx := errgroup.WithContext(ctx)
for _, h := range hosts {
	g.Go(func() error { return checkCtx(ctx, h) })
}
if err := g.Wait(); err != nil {
	return err // the first error; ctx was cancelled for the others
}

Пул воркеров

Фиксированное число горутин, читающих задания из канала, держит конкурентность ограниченной, сколько бы ни было заданий. WaitGroup сообщает, когда все воркеры закончили, а именно тогда можно закрыть канал результатов.

Порядок трёх частей важен:

  • main должен получать результаты, пока работают воркеры. Если бы main вызвал wg.Wait() прямо перед чтением, воркеры заблокировались бы на отправке в results, никогда не дошли бы до Done, и всё зависло бы. Поэтому Wait выполняется в отдельной горутине.
  • close(results) происходит только после Wait, так что ни один воркер не отправит в закрытый канал.
  • Подача заданий тоже идёт в горутине, поэтому подача и сбор перекрываются по времени.

Какой воркер обработал какое задание, меняется от запуска к запуску, поэтому программа перед печатью сортирует по заданию. Всё, что она печатает, детерминировано.

WaitGroup, канал или errgroup

Что нужноЧто использовать
Дождаться N горутин, результаты в ячейках по индексуsync.WaitGroup
Дождаться одной горутиныканал done или сам канал результата
Результаты потоком по мере готовностиканал, закрываемый после wg.Wait()
Остановить всё при первой ошибкеerrgroup.WithContext
Остановить всё по таймауту или отмене вызывающим кодомcontext.Context плюс WaitGroup или errgroup

В Go 1.25 появился wg.Go(func() { ... }), который сам делает Add(1) и отложенный Done. Код для Go 1.24 и раньше, включая редактор на этой странице, использует явную форму, показанную выше.

Частые ошибки

  • wg.Add(1) внутри горутины. Wait может вернуться раньше, чем он выполнится.
  • Забытый Done при раннем возврате. Всегда defer wg.Done().
  • Передача WaitGroup по значению. Используйте указатель; go vet отмечает копию.
  • Ожидание в той же горутине, которая должна вычитывать канал. Вынесите wg.Wait() вместе с close в отдельную горутину.
  • Повторное использование WaitGroup до возврата предыдущего Wait. Начинайте новый цикл вызовов Add только после того, как Wait завершился.

Часто задаваемые вопросы

Как работает sync.WaitGroup в Go?

WaitGroup это счётчик. wg.Add(n) увеличивает его, wg.Done() уменьшает на единицу, а wg.Wait() блокируется, пока он не станет нулём. Вызывайте Add перед запуском каждой горутины, defer wg.Done() внутри неё и Wait там, где нужно, чтобы всё завершилось.

Передавать WaitGroup по значению или по указателю?

По указателю (*sync.WaitGroup) или пусть горутины захватывают его в замыкании. У копии свой счётчик, поэтому Done на копии не доходит до оригинала, и Wait блокируется навсегда. go vet сообщает об этой ошибке как «passes lock by value».

Откуда берётся «sync: negative WaitGroup counter»?

Вызовов Done больше, чем Add. Обычно горутина вызывает Done дважды (один раз через defer и один раз явно) или на каком-то пути пропущен Add(1). Программа паникует, потому что счётчик больше не может сообщить ничего правдивого.

Как получить ошибки из горутин, запущенных с WaitGroup?

WaitGroup не несёт ни результатов, ни ошибок. Дайте каждой горутине свою ячейку в слайсе ошибок и объедините их после Wait (например, через errors.Join) или используйте golang.org/x/sync/errgroup, чей Wait возвращает первую ошибку и может отменить остальные через контекст.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ