Что будет, если не закрывать response.Body в Go? Утечка памяти и другие проблемы
Почему закрытие response.Body критически важно
Вы правы, что в коротких скриптах это может не проявляться, но в реальных приложениях (веб-серверы, долгоиграющие демоны) игнорирование закрытия тела ответа приводит к серьёзным проблемам. Давайте разберёмся по порядку.
Что происходит, когда вы не закрываете Body
Каждый HTTP-ответ в Go использует TCP-соединение. Когда вы читаете тело ответа, соединение остаётся открытым для повторного использования (keep-alive). Но если вы не прочитали тело полностью и не закрыли его, соединение зависает в неопределённом состоянии. Go не может вернуть его в пул соединений, и оно остаётся открытым до тайм-аута (обычно 30-90 секунд).
Вот что на самом деле происходит:
- Утечка горутин — для каждого незакрытого ответа создаётся внутренняя горутина, которая ждёт чтения или закрытия. Она не завершится, пока не будет закрыт Body.
- Утечка файловых дескрипторов — каждое TCP-соединение занимает файловый дескриптор. На Linux по умолчанию их 1024 на процесс. Быстро достигнете лимита.
- Утечка памяти — буферы чтения и структуры соединения не освобождаются.
- Замедление работы — пул HTTP-соединений засоряется "битыми" соединениями, новые запросы вынуждены создавать новые TCP-соединения, что медленнее.
Правильный паттерн
Всегда используйте defer сразу после проверки ошибки:
resp, err := http.Get("https://api.example.com/data")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data))
Важные нюансы
Даже если вы прочитали тело полностью, всё равно нужно закрыть Body. Это сигнал для Go: "я закончил, соединение можно переиспользовать".
Особый случай — когда вы НЕ читаете тело (например, при обработке ошибок). В этом случае нужно либо прочитать и отбросить данные, либо явно закрыть Body:
resp, _ := http.Get("https://api.example.com/data")
defer resp.Body.Close()
// Не читаем тело — плохо! Соединение не вернётся в пул.
// Правильно:
io.Copy(io.Discard, resp.Body) // читаем и выбрасываем
Как отследить проблему
Включите HTTP-трассировку или используйте pprof для проверки количества открытых соединений:
import _ "net/http/pprof"
// Затем: go tool pprof http://localhost:6060/debug/pprof/goroutine
Итог: всегда закрывайте response.Body. Это не просто хорошая практика, а обязательное требование для стабильной работы Go-приложений. Даже если ваш скрипт работает секунду — привыкайте к правильному паттерну, чтобы не допускать ошибок в больших проектах.
<<Альтернативный взгляд: когда можно не закрывать
Технически, если ваша программа завершается сразу после получения данных (одноразовый скрипт), ОС сама закроет все открытые ресурсы. Но это очень плохая привычка.
Есть один легальный случай — если вы используете http.Client с кастомным транспортом, который не поддерживает keep-alive:
tr := &http.Transport{
DisableKeepAlives: true,
}
client := &http.Client{Transport: tr}
resp, _ := client.Get("https://api.example.com")
defer resp.Body.Close()
// В этом случае соединение закрывается сразу после чтения,
// но Body всё равно нужно закрыть для освобождения ресурсов.
Но даже в этом случае закрытие обязательно — без него вы не получите ошибку, если чтение прервалось, и буферы останутся в памяти.
Мой совет: никогда не пытайтесь оптимизировать закрытие Body. Пишите defer resp.Body.Close() всегда. Это занимает одну строку, но спасает от часов отладки в будущем. Go-сообщество единодушно: это обязательный шаблон, а не опциональная рекомендация.