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

Ответ

Классический и наиболее идиоматичный способ в 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 и обработку ошибок.

Ответ 18+ 🔞

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

Паттерн-то какой: WaitGroup + Каналы

  1. sync.WaitGroup: Это, блядь, такой счетчик, типа "сколько горутин ещё пашут". Главная программа на него смотрит и ждёт, пока он в ноль не уйдёт, как будто ждёт, когда все мудаки с обеда вернутся.
  2. Канал: Это уже труба, куда каждая горутина свой результат суёт. Потокобезопасно, без толкотни, всё чинно-благородно, если, конечно, не забыть её закрыть, а то вечно виснем потом, сука.
package main

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

// worker делает вид, что работает, и пихает результат в канал
func worker(id int, wg *sync.WaitGroup, results chan<- string) {
    defer wg.Done() // Гарантированно говорим "я всё, свободен", даже если посередине пиздец случится
    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)
    }

    // А вот тут хитрость, ёпта! Запускаем анонимную горутину-сторожа.
    // Её задача — дождаться, пока ВСЕ воркеры отпишутся (wg.Wait()),
    // и тогда БЛЯДЬ ЗАКРЫТЬ КАНАЛ. Это важно, а то в цикле чтения ниже зависнем навеки.
    go func() {
        wg.Wait()
        close(results)
    }()

    // А тут главная программа просто читает из канала, пока он не закроется.
    // Красиво, просто, ни одной лишней проверки.
    fmt.Println("Сижу, жду, когда работа сделается...")
    for res := range results {
        fmt.Println("Прилетело:", res)
    }
    fmt.Println("Всё, приехали. Можно расходиться.")
}

Если хочешь поумничать: golang.org/x/sync/errgroup

А вот если тебе ещё и ошибки ловить надо, да чтобы всё по щелчку отменялось, если один мудак всё проебал — тогда есть пакетик errgroup. Он, сука, как продвинутая обёртка над WaitGroup, сам и ошибки соберёт, и контекст прикрутить можно. Для серьёзных пацанов, одним словом.