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