Как собрать и обработать результаты выполнения нескольких параллельных горутин?

Ответ

Классический и наиболее идиоматичный способ в Go — использовать комбинацию sync.WaitGroup для ожидания завершения всех горутин и каналов для безопасной передачи результатов.

Основной паттерн: WaitGroup + Channels

  1. sync.WaitGroup: Используется как счетчик активных горутин. Главная горутина ждет, пока счетчик не обнулится.
  2. Канал: Служит потокобезопасной очередью для сбора результатов от каждой горутины.
package main

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

// worker выполняет "работу" и отправляет результат в канал
func worker(id int, wg *sync.WaitGroup, results chan<- string) {
    defer wg.Done() // Уменьшаем счетчик WaitGroup по завершении
    fmt.Printf("Воркер %d начал работуn", id)
    time.Sleep(time.Second) // Имитация работы
    results <- fmt.Sprintf("Результат от воркера %d", id)
    fmt.Printf("Воркер %d закончил работуn", id)
}

func main() {
    const numWorkers = 5
    var wg sync.WaitGroup
    // Буферизированный канал, чтобы воркеры не блокировались при отправке результата
    results := make(chan string, numWorkers)

    // Запускаем воркеров
    wg.Add(numWorkers)
    for i := 1; i <= numWorkers; i++ {
        go worker(i, &wg, results)
    }

    // Запускаем отдельную горутину, которая закроет канал
    // после того, как все воркеры завершат свою работу.
    // Это позволяет безопасно использовать range для чтения из канала.
    go func() {
        wg.Wait()
        close(results)
    }()

    // Читаем все результаты из канала, пока он не будет закрыт
    fmt.Println("Ожидание результатов...")
    for res := range results {
        fmt.Println("Получен:", res)
    }
    fmt.Println("Все результаты получены.")
}

Альтернатива: golang.org/x/sync/errgroup

Для сценариев, где важна обработка ошибок и контекста (например, отмена всех горутин при первой ошибке), пакет errgroup предоставляет более высокоуровневую и удобную абстракцию. Он инкапсулирует WaitGroup и обработку ошибок.