Конкурентные загрузчики утекают HTTP-соединениями. В чём ошибка с resp.Body.Close()?
Каждая горутина загружает URL и сохраняет результат. Под нагрузкой процесс утекает HTTP-соединениями и в итоге упирается в too many open files.
go func(url string) {
defer wg.Done()
httpClient := &http.Client{Timeout: 5 * time.Second}
resp, err := httpClient.Get(url)
if err != nil {
fmt.Printf("Error fetching %s: %v\n", url, err)
return
}
b, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("Error reading body from %s: %v\n", url, err)
return
}
defer resp.Body.Close()
mu.Lock()
defer mu.Unlock()
results[url] = Result{HttpCode: resp.StatusCode, Body: b}
}(url)
Найдите и исправьте ошибку.
defer resp.Body.Close() стоит после io.ReadAll, а ранний if err != nil возвращает управление, оставляя body незакрытым на пути ошибки. Перенесите defer resp.Body.Close() сразу после проверки ошибки Get, чтобы каждый успешный Get закрывал body и освобождал соединение.
- ✗Ставить
deferClose после проверки ошибки чтения body, из-за чего ошибка чтения возвращает управление с открытым body - ✗Считать, что body запроса не нужно закрывать, если он маленький или уже прочитан
- ✗Считать порядок
deferкосметикой — он устраняет утечку, только если зарегистрирован до любого раннего return
- →Почему незакрытый
resp.Bodyмешает переиспользовать нижележащее TCP-соединение? - →Когда
resp.Bodyможет бытьnil, и нужна ли там защита передClose?
Баг
defer resp.Body.Close() поставлен слишком поздно — после io.ReadAll. Если io.ReadAll вернёт ошибку, goroutine делает return раньше, чем defer зарегистрирован, и body остаётся открытым. Незакрытый body удерживает TCP-соединение, не давая http.Transport вернуть его в пул.
go func(url string) {
defer wg.Done()
httpClient := &http.Client{Timeout: 5 * time.Second}
resp, err := httpClient.Get(url)
if err != nil {
fmt.Printf("Error fetching %s: %v\n", url, err)
return
}
b, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("Error reading body from %s: %v\n", url, err)
return // ❌ body ещё открыт — defer ниже не сработает
}
defer resp.Body.Close() // ❌ зарегистрирован слишком поздно
mu.Lock()
defer mu.Unlock()
results[url] = Result{HttpCode: resp.StatusCode, Body: b}
}(url)
Исправление
Регистрируйте defer resp.Body.Close() сразу после проверки ошибки Get — до любого return:
resp, err := httpClient.Get(url)
if err != nil {
fmt.Printf("Error fetching %s: %v\n", url, err)
return
}
defer resp.Body.Close() // ✅ закроется на любом пути выхода
b, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("Error reading body from %s: %v\n", url, err)
return // ✅ body всё равно закроется
}