Базовый шаблон
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 возвращает первую ошибку и может отменить остальные через контекст.