Как в Go выполнить несколько HTTP-запросов параллельно и дождаться их завершения?

Ответ

Для параллельного выполнения задач и ожидания их завершения в Go чаще всего используются горутины в связке с sync.WaitGroup.

1. Классический подход: sync.WaitGroup

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

  • wg.Add(n): Увеличивает счетчик на n.
  • wg.Done(): Уменьшает счетчик на 1 (обычно вызывается в defer).
  • wg.Wait(): Блокирует горутину, пока счетчик не станет 0.
package main

import (
    "fmt"
    "net/http"
    "sync"
)

func main() {
    urls := []string{
        "https://ya.ru",
        "https://google.com",
        "https://bing.com",
        "https://duckduckgo.com",
    }

    var wg sync.WaitGroup

    for _, url := range urls {
        wg.Add(1) // Увеличиваем счетчик перед запуском горутины
        go func(u string) {
            defer wg.Done() // Уменьшаем счетчик по завершении

            resp, err := http.Get(u)
            if err != nil {
                fmt.Printf("Error fetching %s: %vn", u, err)
                return
            }
            fmt.Printf("Got %s with status %sn", u, resp.Status)
            resp.Body.Close()
        }(url) // Важно передать url как аргумент, чтобы избежать проблем с замыканием!
    }

    wg.Wait() // Ожидаем завершения всех горутин
    fmt.Println("All requests finished.")
}

2. Продвинутый подход: errgroup

Пакет golang.org/x/sync/errgroup упрощает работу с группами горутин, особенно когда нужна обработка ошибок и отмена контекста.

  • Автоматически управляет WaitGroup.
  • Возвращает первую возникшую ошибку.
  • Позволяет отменить все остальные операции через context при возникновении ошибки.
// ... импорты
import (
    "context"
    "golang.org/x/sync/errgroup"
)

// ... в функции main
g, ctx := errgroup.WithContext(context.Background())

for _, url := range urls {
    u := url // Создаем локальную копию для горутины
    g.Go(func() error {
        req, _ := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
        resp, err := http.DefaultClient.Do(req)
        if err != nil {
            return fmt.Errorf("failed to get %s: %w", u, err)
        }
        // ... обработка ответа
        resp.Body.Close()
        return nil
    })
}

if err := g.Wait(); err != nil {
    fmt.Printf("An error occurred: %vn", err)
}

Для ограничения количества одновременных запросов (например, не более 5 за раз) можно использовать буферизованный канал в качестве семафора.

Ответ 18+ 🔞

Слушай, а вот этот твой код с горутинами — это ж классика, блядь! Как эти все вэб-сайты дергать параллельно, чтобы не ждать, пока один за другим, как мудаки, загрузятся.

Ну, первое, что в голову приходит — это sync.WaitGroup. Представь себе, это как такой счетчик, сука. Ты ему говоришь: «Слушай, я сейчас 4 горутины запущу». Он такой: «Ага, окей, wg.Add(4)». А потом каждая горутина, когда свою хуйню сделала, кричит: «Я всё, свободна!» — wg.Done(). А главная горутина сидит на wg.Wait(), как дура, и не двигается дальше, пока этот счетчик в ноль не упадет. Пиздец просто, но работает.

var wg sync.WaitGroup

for _, url := range urls {
    wg.Add(1) // Щас ещё одна побежит, считай её!
    go func(u string) {
        defer wg.Done() // Всё, приехали, я отстрелялась. Обязательно через defer, а то забудешь, кретин!

        resp, err := http.Get(u)
        if err != nil {
            fmt.Printf("Error fetching %s: %vn", u, err)
            return
        }
        fmt.Printf("Got %s with status %sn", u, resp.Status)
        resp.Body.Close()
    }(url) // Смотри, передавай урл как аргумент, а то все горутины на последний url набросятся, будет овердохуища смеха!
}

wg.Wait() // Сидим тут, блядь, ждём, пока все долбаные запросы не отработают.
fmt.Println("All requests finished.")

Но это, блядь, для простых случаев. А если тебе ещё и ошибки ловить, и чтобы при первой же ошибке все остальные горутины пошли нахуй? Тут в игру вступает errgroup. Это такая обёрточка, ёпта, просто песня!

g, ctx := errgroup.WithContext(context.Background())

for _, url := range urls {
    u := url // Опять эта хуйня с замыканием, делай локальную копию!
    g.Go(func() error {
        // Контекст прокидываем, чтобы если одна операция сломалась — остальные тоже отменились.
        req, _ := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
        resp, err := http.DefaultClient.Do(req)
        if err != nil {
            return fmt.Errorf("failed to get %s: %w", u, err) // Вернул ошибку — вся группа накрылась медным тазом.
        }
        resp.Body.Close()
        return nil
    })
}

if err := g.Wait(); err != nil {
    fmt.Printf("An error occurred: %vn", err) // Поймали первую же ошибку — и всем пиздец.
}

Ах да, самый важный момент! Если ты запустишь 100500 горутин на 100500 запросов одновременно, тебя ебанут по IP, или твое приложение сожрёт всю память. Надо ограничивать, сука! Тут тебе в помощь — семафор через буферизованный канал. Создаёшь канал, скажем, на 5 мест. Прежде чем запустить горутину, пытаешься в него что-то запихнуть (sem <- struct{}{}). Если там уже 5 штук — ждёшь, пока одна не освободится. Горутина, завершаясь, выкидывает из канала пустышку (<-sem). И всё, магия! Одновременно только 5 горутин будут пастись. Красота, блядь!