Какие существуют способы для ожидания завершения нескольких горутин в Go?

Ответ

Основным и идиоматичным способом является использование sync.WaitGroup.

1. Использование sync.WaitGroup

WaitGroup — это счетчик, который блокирует выполнение до тех пор, пока его внутреннее значение не станет равным нулю.

Как это работает:

  1. Перед запуском каждой горутины вызывается wg.Add(1), чтобы увеличить счетчик.
  2. Внутри горутины с помощью defer wg.Done() мы гарантируем, что счетчик уменьшится при выходе из функции.
  3. В основной горутине wg.Wait() блокирует выполнение, пока счетчик не обнулится.
package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {
    // Уменьшаем счетчик, когда горутина завершается
    defer wg.Done()

    fmt.Printf("Worker %d startingn", id)
    time.Sleep(time.Second) // Симуляция работы
    fmt.Printf("Worker %d donen", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 3; i++ {
        // Увеличиваем счетчик перед запуском горутины
        wg.Add(1)
        go worker(i, &wg)
    }

    // Ожидаем, пока все горутины не вызовут Done()
    wg.Wait()

    fmt.Println("All workers have finished.")
}

Важные моменты:

  • wg.Add() всегда вызывается до запуска горутины, чтобы избежать состояния гонки.
  • WaitGroup следует передавать в функции по указателю (*sync.WaitGroup).

2. Альтернативные подходы

  • Каналы: Можно использовать канал для сигнализации о завершении. Каждая горутина по завершении отправляет значение в канал. Основная горутина читает из канала столько раз, сколько горутин было запущено.

    done := make(chan struct{}, 3) // Буферизованный канал
    for i := 0; i < 3; i++ {
        go func() {
            // ... работа ...
            done <- struct{}{}
        }()
    }
    for i := 0; i < 3; i++ {
        <-done
    }
  • Пакет errgroup: Это более продвинутый способ, особенно полезный, когда нужно обрабатывать ошибки из горутин и управлять контекстом (например, для отмены всех горутин при первой ошибке).

    import "golang.org/x/sync/errgroup"
    
    g, ctx := errgroup.WithContext(context.Background())
    for i := 0; i < 3; i++ {
        g.Go(func() error {
            // ... работа ...
            // return err // При ошибке все остальные горутины будут отменены
            return nil
        })
    }
    if err := g.Wait(); err != nil {
        // Обработка первой возникшей ошибки
    }