Параллельный fetch URL
Условие
Напишите функцию, которая параллельно делает HTTP запросы к нескольким URL и возвращает результаты. Должен поддерживаться context для отмены и ограничение количества одновременных запросов.
Сигнатура
type Result struct {
URL string
Body []byte
Error error
}
func fetchURLs(ctx context.Context, urls []string, maxConcurrent int) []Result
Требования
- Параллельная обработка URL
- Ограничение количества одновременных горутин (maxConcurrent)
- Поддержка отмены через context
- Возврат результатов для всех URL (включая ошибки)
Пример
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
urls := []string{"https://google.com", "https://github.com"}
results := fetchURLs(ctx, urls, 5)
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение: Параллельный fetch URL
Описание задачи
Нужно реализовать параллельный HTTP-клиент, который:
- Делает запросы к нескольким URL одновременно
- Ограничивает количество одновременных горутин (примерно как connection pool)
- Поддерживает отмену через context
- Возвращает результаты, включая ошибки
Основная идея
Используем semaphore pattern (семафор на канале) для ограничения одновременных горутин. Семафор — это канал с буфером размером maxConcurrent. Перед началом работы горутина "занимает" место в семафоре, после завершения — освобождает.
Реализация
import (
"context"
"fmt"
"io"
"net/http"
"sync"
)
type Result struct {
URL string
Body []byte
Error error
}
func fetchURLs(ctx context.Context, urls []string, maxConcurrent int) []Result {
// Результаты храним в slice
results := make([]Result, len(urls))
// WaitGroup для ожидания всех горутин
var wg sync.WaitGroup
// Семафор: канал с буфером ограничивает одновременные запросы
semaphore := make(chan struct{}, maxConcurrent)
// Обработка каждого URL
for i, url := range urls {
wg.Add(1)
go func(index int, urlStr string) {
defer wg.Done()
// Занимаем место в семафоре
select {
case semaphore <- struct{}{}:
// Успешно занял место
case <-ctx.Done():
// Context отменён, заканчиваем
results[index] = Result{
URL: urlStr,
Error: ctx.Err(),
}
return
}
// Обязательно освобождаем место
defer func() { <-semaphore }()
// Создаём HTTP запрос с context
req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil)
if err != nil {
results[index] = Result{
URL: urlStr,
Error: err,
}
return
}
// Выполняем запрос
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
results[index] = Result{
URL: urlStr,
Error: err,
}
return
}
defer resp.Body.Close()
// Читаем тело ответа
body, err := io.ReadAll(resp.Body)
if err != nil {
results[index] = Result{
URL: urlStr,
Error: err,
}
return
}
// Успешный результат
results[index] = Result{
URL: urlStr,
Body: body,
}
}(i, url)
}
// Ждём все горутины
wg.Wait()
return results
}
Как это работает
Семафор — это канал semaphore := make(chan struct{}, maxConcurrent) с буфером размером maxConcurrent:
-
semaphore <- struct{}{}— попытка отправить значение в канал (занять место)- Если есть место — сразу проходит
- Если буфер полный — блокируется, пока место не освободится
-
<-semaphore— чтение из канала (освобождение места)- Выполняется в defer, чтобы гарантированно освободить место
-
Если context отменён во время ожидания в select, выходим с ошибкой
WaitGroup (sync.WaitGroup) — синхронизирует завершение всех горутин:
wg.Add(1)— регистрируем новую горутинуwg.Done()— сигнализируем о завершении (в defer)wg.Wait()— блокируется до завершения всех
http.NewRequestWithContext — создаёт запрос с контекстом, который обрывает запрос при отмене
Пример использования
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
urls := []string{
"https://google.com",
"https://github.com",
"https://example.com",
}
results := fetchURLs(ctx, urls, 2) // максимум 2 одновременных запроса
for _, result := range results {
if result.Error != nil {
fmt.Printf("URL %s: ERROR %v\n", result.URL, result.Error)
} else {
fmt.Printf("URL %s: %d bytes\n", result.URL, len(result.Body))
}
}
Ключевые моменты
- Семафор вместо worker pool: проще и более идиоматично в Go
- defer для освобождения семафора: гарантирует освобождение даже при паник
- Context поддержка: запрос отменяется при cancel или timeout
- Сохранение порядка: результаты в том же порядке, что и входные URL
- Обработка ошибок: все ошибки (HTTP, IO, context) попадают в Result.Error
- Потокобезопасность: slice results не требует синхронизации, так как каждая горутина пишет в свой индекс
Это pattern часто используется в production Go коде для API и веб-скрейпинга.