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

Ответ

В Go можно связать несколько запросов с помощью context.Context для управления таймаутами и отменой, а также sync.WaitGroup для ожидания завершения всех горутин.

Пример:

package main

import (
    "context"
    "fmt"
    "io"
    "net/http"
    "sync"
    "time"
)

func fetchAll(ctx context.Context, urls []string) ([]string, error) {
    var wg sync.WaitGroup
    results := make([]string, len(urls))
    // Канал для передачи первой ошибки. Буферизация в 1 позволяет отправить ошибку без блокировки.
    errChan := make(chan error, 1)

    for i, url := range urls {
        wg.Add(1)
        go func(i int, url string) {
            defer wg.Done()

            // Создаем запрос с контекстом для отмены
            req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
            if err != nil {
                // Пытаемся отправить ошибку в канал, но не блокируемся, если канал уже занят
                select {
                case errChan <- fmt.Errorf("failed to create request for %s: %w", url, err):
                default:
                }
                return
            }

            resp, err := http.DefaultClient.Do(req)
            if err != nil {
                select {
                case errChan <- fmt.Errorf("failed to fetch %s: %w", url, err):
                default:
                }
                return
            }
            defer resp.Body.Close()

            body, err := io.ReadAll(resp.Body)
            if err != nil {
                select {
                case errChan <- fmt.Errorf("failed to read body for %s: %w", url, err):
                default:
                }
                return
            }
            results[i] = string(body)
        }(i, url)
    }

    // Ожидаем завершения всех горутин
    wg.Wait()

    // Проверяем, была ли отправлена ошибка
    select {
    case err := <-errChan:
        return nil, err
    default:
        // Если ошибок не было, возвращаем результаты
        return results, nil
    }
}

func main() {
    urls := []string{
        "https://jsonplaceholder.typicode.com/posts/1",
        "https://jsonplaceholder.typicode.com/posts/2",
        "https://jsonplaceholder.typicode.com/posts/3",
    }

    // Создаем контекст с таймаутом
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel() // Важно вызвать cancel для освобождения ресурсов контекста

    fmt.Println("Начинаем параллельные запросы...")
    data, err := fetchAll(ctx, urls)

    if err != nil {
        fmt.Printf("Ошибка при выполнении запросов: %vn", err)
    } else {
        fmt.Println("Все запросы успешно выполнены:")
        for i, res := range data {
            fmt.Printf("URL %d: %s...n", i+1, res[:50]) // Выводим первые 50 символов
        }
    }

    // Пример с отменой/таймаутом (можно раскомментировать для теста)
    // ctxTimeout, cancelTimeout := context.WithTimeout(context.Background(), 1*time.Millisecond)
    // defer cancelTimeout()
    // fmt.Println("nТестируем таймаут...")
    // _, errTimeout := fetchAll(ctxTimeout, urls)
    // if errTimeout != nil {
    //  fmt.Printf("Ошибка таймаута: %vn", errTimeout)
    // }
}

Ключевые моменты:

  • context.Context: Используется для передачи сигналов отмены или таймаута всем горутинам. Если контекст отменяется (например, из-за таймаута или явного вызова cancel()), http.Client автоматически прерывает выполняющиеся запросы.
  • sync.WaitGroup: Позволяет основной горутине дождаться завершения всех запущенных параллельных горутин.
  • Канал ошибок (errChan): Используется для передачи первой возникшей ошибки из любой горутины в основную. Буферизация канала в 1 позволяет отправить ошибку без блокировки, а select { ... default: } предотвращает блокировку, если ошибка уже была отправлена.
  • Обработка ошибок: Важно проверять ошибки при создании запроса (http.NewRequestWithContext), выполнении запроса (http.DefaultClient.Do) и чтении тела ответа (io.ReadAll). В примере эти ошибки перехватываются и отправляются в errChan.