Avatar ·

Что будет, если не закрывать response.Body в Go? Утечка памяти и другие проблемы

Всем привет! Я только начал изучать Go и пишу простой HTTP-клиент для получения данных с API. У меня есть такой код: ```go resp, err := http.Get("https://api.example.com/data") if err != nil { log.Fatal(err) } // Я не закрываю resp.Body data, _ := io.ReadAll(resp.Body) fmt.Println(string(data)) ``` Всё работает, данные приходят. Но я заметил, что во многих примерах на Stack Overflow и в документации Go пишут `defer resp.Body.Close()`. Честно говоря, я не понимаю, зачем это нужно, если программа завершается сразу после чтения данных? Что может случиться плохого, если я забуду закрыть `response.Body`? Утечка памяти? Замедление программы? Или это вообще не критично для маленьких скриптов? Объясните, пожалуйста, на пальцах, что происходит под капотом. Заранее спасибо! <<>>

Почему закрытие 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-сообщество единодушно: это обязательный шаблон, а не опциональная рекомендация.