Ответ
В 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
.