GoJuniorCoding
Что такое WaitGroup и как он используется?
sync.WaitGroup позволяет горутине ждать завершения группы других горутин: Add(n) добавляет счётчик, Done() декрементирует его, Wait() блокируется пока счётчик не достигнет 0.
sync.WaitGroup
sync.WaitGroup из стандартной библиотеки — механизм синхронизации, позволяющий дождаться завершения набора горутин. Три метода API: Add(delta int), Done() и Wait().
Базовый пример
package main
import (
sync "sync"
"fmt"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // вызывается при выходе из функции
fmt.Printf("worker %d start\n", id)
time.Sleep(100 * time.Millisecond)
fmt.Printf("worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1) // инкремент перед запуском горутины
go worker(i, &wg) // передаём указатель!
}
wg.Wait() // блокируется, пока все Done() не вызваны
fmt.Println("all workers finished")
}
WaitGroup + канал для сбора результатов
func fanOut(items []int) []int {
var wg sync.WaitGroup
results := make(chan int, len(items))
for _, item := range items {
wg.Add(1)
go func(v int) {
defer wg.Done()
results <- v * v
}(item)
}
// Закрываем канал, когда все горутины завершились
go func() {
wg.Wait()
close(results)
}()
var out []int
for r := range results {
out = append(out, r)
}
return out
}
WaitGroup с контекстом и обработкой ошибок
import (
"context"
"errors"
"sync"
)
func runAll(ctx context.Context, tasks []func(context.Context) error) error {
var (
wg sync.WaitGroup
once sync.Once
firstErr error
)
for _, task := range tasks {
wg.Add(1)
t := task
go func() {
defer wg.Done()
if err := t(ctx); err != nil {
once.Do(func() { firstErr = err })
}
}()
}
wg.Wait()
return firstErr
}
Для более сложных случаев используйте golang.org/x/sync/errgroup — он оборачивает WaitGroup с поддержкой ошибок и отмены контекста.
Правила использования
Addвызывается до запуска горутины — иначе возможна гонка междуAddиWait.- WaitGroup передаётся по указателю — копирование нарушает внутреннее состояние.
defer wg.Done()— стандартный идиом; гарантирует вызов даже при панике.- Счётчик не должен уходить в минус —
Doneбез предшествующегоAddвызовет панику.
Подводные камни
- Передача WaitGroup по значению (не указателю) — данные о счётчике теряются,
Waitникогда не разблокируется или вызывает панику. Addпосле старта горутины: горутина может завершиться раньше, чемAddбудет вызван, иWaitразблокируется раньше времени.- Попытка повторно использовать WaitGroup до того, как
Waitвернулся — приводит к гонке данных. - Паника в горутине:
defer wg.Done()не спасёт остальной код от зависания наWait, если горутина паникует без recover — нуженdefer func() { recover(); wg.Done() }(). - Для сбора ошибок из горутин WaitGroup не достаточен — нужен канал или
errgroup. - WaitGroup не ограничивает параллелизм — для ограничения числа одновременных горутин используйте semaphore-паттерн с буферизованным каналом.
Common mistakes
- Использовать WaitGroup как канал данных или ошибок.
- Вызывать Add после старта goroutine без понимания гонки.
- Забывать Done на каждом пути выхода.
What the interviewer is testing
- Показывает каноническую последовательность Add, go, Done, Wait.
- Понимает ограничения WaitGroup относительно ошибок и отмены.
- Не путает WaitGroup с mutex или channel.