> Как используется WaitGroup для ожидания завершения горутин в Go (Go)

Уровень: senior · Роль: backend · Категория: Технические вопросы

Компании: Т-Банк, Моторика

Стек: Go

> Пример ответа

Короткий ответ

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

Подробное объяснение

WaitGroup работает как атомарный счётчик. Основные методы:

  • Add(delta int) - увеличивает счётчик на delta. Обычно вызывается перед запуском горутины.
  • Done() - эквивалент Add(-1), вызывается в конце работы горутины.
  • Wait() - блокирует выполнение до тех пор, пока счётчик не станет равным нулю.

Ключевые правила:

  1. Add должен вызываться до Wait, иначе возможна гонка или deadlock.
  2. Add лучше вызывать в основной горутине до go func(), а не внутри самой горутины - это гарантирует, что счётчик будет корректно установлен до начала ожидания.
  3. Done рекомендуется вызывать через defer, чтобы гарантировать уменьшение счётчика даже при панике внутри горутины.
  4. WaitGroup нельзя копировать после использования - передавайте указатель.

Типичный паттерн:

GO
var wg sync.WaitGroup
for i := 0; i < n; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// работа
}()
}
wg.Wait()

На практике

В реальном коде WaitGroup часто комбинируют с каналами или контекстом для ограничения конкурентности. Например, при пуле воркеров:

GO
jobs := make(chan int)
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go worker(jobs, &wg)
}
for j := 0; j < 10; j++ {
jobs <- j
}
close(jobs)
wg.Wait()

Важно помнить, что WaitGroup не отменяет горутины - он только ждёт их завершения. Для отмены используйте context.Context в связке с select.

Пример кода

GO
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
time.Sleep(time.Duration(id) * 100 * time.Millisecond)
fmt.Printf("горутина %d завершена\n", id)
}(i)
}
wg.Wait()
fmt.Println("все горутины завершены")
}

Как отвечать на собеседовании

Начните с определения и назначения. Затем опишите три метода и порядок их использования. Обязательно упомяните правило: Add до запуска горутины, Done через defer. Приведите простой пример с циклом. Если спросят про ограничения - скажите, что WaitGroup не умеет отменять горутины и не передаёт ошибки, для этого есть errgroup из golang.org/x/sync. Также упомяните, что WaitGroup нельзя копировать после использования - это частая ошибка.

Что проверяет интервьюер

  • Понимание базовой механики счётчика.
  • Знание правильного порядка вызова Add и Wait.
  • Осознание важности defer wg.Done().
  • Понимание ограничений WaitGroup и альтернатив (errgroup, каналы, context).
  • Умение объяснить, почему Add нельзя вызывать внутри горутины при конкурентном доступе.

Типичные ошибки

  • Вызов Add внутри горутины - гонка данных и возможный deadlock.
  • Забытый Done - программа зависнет на Wait.
  • Копирование WaitGroup - неопределённое поведение.
  • Использование WaitGroup для передачи данных между горутинами - для этого есть каналы.
  • Ожидание завершения горутин без обработки паник - если горутина паникует без defer, счётчик не уменьшится.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью