Параллельная загрузка списка URL с ранним завершением при ошибке

Запросам нужен общий context: первая ошибка отменяет остальные. Для указанной сигнатуры вызов должен быть err := download(...), без второго результата.

Короткий ответ

Что ответить на собеседовании

Запросам нужен общий context: первая ошибка отменяет остальные. Для указанной сигнатуры вызов должен быть err := download(...), без второго результата.

Подробный разбор

Ответ с пояснениями

Условие

Дан массив строк, каждая из которых представляет собой URL. Требуется написать функцию download(urls []string) error, которая будет одновременно скачивать содержимое всех указанных адресов. Если хотя бы один запрос завершится с ошибкой, функция должна немедленно вернуть эту ошибку; если все запросы прошли успешно — вернуть nil. Необходимо обеспечить корректную работу в многопоточной среде и реализовать механизм прекращения всех остальных запросов при возникновении первой ошибки.

func main() {
 _, err := download([]string{
  "https://example.com/e25e26d3-6aa3-4d79-9ab4-fc9b71103a8c.xml",
  "https://example.com/a601590e-31c1-424a-8ccc-dec5fb35c0f6.xml",
  "https://example.com/1cf0dd69-a3e5-4682-84e3-dfe22ca771f4.xml",
  "https://example.com/ceb566f2-a234-4cb8-9466-4a26f1363aa8.xml",
  "https://example.com/b6ed16d7-cb3d-4cba-b81a-01a789d3a914.xml",
 })
 if err != nil {
  panic(err)
 }
}

func download(urls []string) error {
 return nil
}

Ответ

Запросам нужен общий context: первая ошибка отменяет остальные. Для указанной сигнатуры вызов должен быть err := download(...), без второго результата.

Ниже скачивание означает чтение и отбрасывание тела; для сохранения файлов нужен отдельный контракт. Буфер результатов позволяет завершать горутины после раннего возврата.

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

func download(urls []string) error {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    client := &http.Client{Timeout: 30 * time.Second}
    results := make(chan error, len(urls))
    for _, url := range urls {
        go func(url string) {
            results <- func() error {
                req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
                if err != nil { return err }
                resp, err := client.Do(req)
                if err != nil { return err }
                defer resp.Body.Close()
                if resp.StatusCode < 200 || resp.StatusCode >= 300 {
                    return fmt.Errorf("HTTP status %d", resp.StatusCode)
                }
                _, err = io.Copy(io.Discard, resp.Body)
                return err
            }()
        }(url)
    }
    for range urls {
        if err := <-results; err != nil {
            return err
        }
    }
    return nil
}

Функция возвращает при получении первой ошибки и отменяет запросы, не ожидая завершения всех горутин. Фактическая остановка транспорта кооперативная. Для большого списка добавляют ограничение параллелизма.

ИИ-помощник для собеседований

Хочешь уверенно проходить собеседования?

Попробуй ИИ-помощник для собеседований: слышит вас и собеседника, анализирует экран, подсказывает ответы в реальном времени, работает без VPN и не попадает в захват экрана.

Подробнее