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