Код-ревью Middle Горутины и планировщик GMP спрашивают часто обновлено

Код-ревью Go: параллельная загрузка URL

Вопрос

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

func FetchAll(urls []string) []int {
    var sizes []int
    for _, u := range urls {
        go func() {
            resp, err := http.Get(u)
            if err != nil {
                return
            }
            body, _ := io.ReadAll(resp.Body)
            sizes = append(sizes, len(body))
        }()
    }
    time.Sleep(2 * time.Second)
    return sizes
}

Коротко

Исправленная версия: Ограничение параллелизма, таймауты через клиент и контекст, закрытие тела, запись по индексу без гонки и ожидание через errgroup с возвратом первой ошибки.

Что должен найти ревьюер

  1. Производительностьстрока 4
    Горутина на каждый URL без ограничения: на тысячах адресов это тысячи одновременных соединений. Нужен пул или семафор.
  2. Ошибкастрока 5
    http.Get использует DefaultClient без таймаута: зависший сервер подвесит горутину навсегда. Нужен свой клиент с Timeout или запрос с контекстом.
  3. Ошибкастрока 7
    Ошибка проглатывается молча: вызывающий не узнает, что часть страниц не скачалась.
  4. Ошибкастрока 9
    Тело ответа не закрывается (defer resp.Body.Close()): соединения не возвращаются в пул, утекают сокеты. Ошибка ReadAll тоже игнорируется.
  5. Ошибкастрока 10
    Гонка данных: несколько горутин делают append в общий слайс без синхронизации. Плюс порядок результатов не совпадает с порядком urls. Нужно писать в sizes[i] заранее созданного слайса.
  6. Ошибкастрока 13
    Sleep не гарантирует, что горутины закончили: медленные ответы потеряются, быстрые заставят ждать зря. Нужен sync.WaitGroup или errgroup.

Разбор

Исправленная версия:

func FetchAll(ctx context.Context, client *http.Client, urls []string) ([]int, error) {
    sizes := make([]int, len(urls))
    g, ctx := errgroup.WithContext(ctx)
    g.SetLimit(8)
    for i, u := range urls {
        g.Go(func() error {
            req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
            if err != nil {
                return err
            }
            resp, err := client.Do(req)
            if err != nil {
                return fmt.Errorf("get %s: %w", u, err)
            }
            defer resp.Body.Close()
            n, err := io.Copy(io.Discard, resp.Body)
            sizes[i] = int(n)
            return err
        })
    }
    return sizes, g.Wait()
}

Ограничение параллелизма, таймауты через клиент и контекст, закрытие тела, запись по индексу без гонки и ожидание через errgroup с возвратом первой ошибки.

Ответьте сами, а не прочитайте

В тренажёре вопрос попадёт в повторения и вернётся, когда начнёт забываться. Плюс билеты с зачётом: 20 вопросов, не больше 2 ошибок.