← Назад к вопросам

Параллельный fetch URL

2.0 Middle🔥 51 комментариев
#Конкурентность и горутины

Условие

Напишите функцию, которая параллельно делает 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)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение: Параллельный 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:

  1. semaphore <- struct{}{} — попытка отправить значение в канал (занять место)

    • Если есть место — сразу проходит
    • Если буфер полный — блокируется, пока место не освободится
  2. <-semaphore — чтение из канала (освобождение места)

    • Выполняется в defer, чтобы гарантированно освободить место
  3. Если 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 и веб-скрейпинга.

Параллельный fetch URL | PrepBro