В реальном коде мы постоянно берём внешние ресурсы: открываем соединения с базой данных, делаем HTTP-запросы, открываем файлы. Каждый такой ресурс нужно обязательно освобождать. Если закрытие забыть, начинается утечка: соединение с базой висит открытым, пул соединений постепенно заполняется, новые запросы ждут, приложение подвисает, а база сыплет ошибками «too many connections». То же самое с HTTP и файлами: дескрипторы остаются занятыми, и рано или поздно система перестаёт нормально работать.
Пример работы с базой данных
defer
решает эту боль очень просто. Сразу рядом с «взятием» ресурса можно объявить, что он должен быть закрыт в конце функции. Дальше можем реализовывать логику как нужно, выходишь по ошибкам или по успеху, — уборка всё равно произойдёт автоматически. Это значит, что не нужно держать в голове десятки мест, где поставить Close()
, и не нужно бояться, что при рефакторинге забудешь что-то убрать или добавить.
То есть: взял ресурс → тут же рядом поставил deferred-уборку → и забыл.
При любом выходе — успешном, через return
, при ошибке, даже при панике — deferred-вызовы выполняются.
Пример без defer
func getUser(id int) (*User, error) {
db, err := sql.Open("postgres", "…")
if err != nil {
return nil, err
}
// тут нужно не забыть закрыть соединение
// db.Close()
row := db.QueryRow("SELECT id, name FROM users WHERE id = $1", id)
var u User
if err := row.Scan(&u.ID, &u.Name); err != nil {
// ой, ошибка! а db.Close() забыли вызвать
return nil, err
}
// если бы вспомнили — написали бы тут db.Close()
return &u, nil
}
Такой код формально работает, но поддерживать его тяжело. В каждом пути выхода нужно помнить о db.Close()
, и одна забытая строчка обернётся проблемой на продакшене.
Пример с defer
func getUser(id int) (*User, error) {
db, err := sql.Open("postgres", "…")
if err != nil {
return nil, err
}
defer db.Close() // гарантированно закроется при любом выходе
row := db.QueryRow("SELECT id, name FROM users WHERE id = $1", id)
var u User
if err := row.Scan(&u.ID, &u.Name); err != nil {
return nil, err // можно спокойно выйти — db всё равно закроется
}
return &u, nil
}
Здесь принцип простой: взял ресурс → сразу же повесил его освобождение через defer
→ дальше пишешь логику и не думаешь о Close()
.
Та же история с HTTP
resp, err := http.Get("https://example.com")
if err != nil {
panic(err)
}
defer resp.Body.Close() // освободит соединение
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
Если забыть Close()
, то соединение останется занято, и пул начнёт переполняться. С defer
всё решается автоматически.
Где ставить defer
defer
используют сразу после того, как ты получил ресурс, который нужно освободить. \
То есть правило простое:
- Открыл файл → сразу же
defer file.Close()
. - Сделал HTTP-запрос → сразу же
defer resp.Body.Close()
. - Подключился к базе → сразу же
defer db.Close()
. - Залочил мьютекс → сразу же
defer mu.Unlock()
.
Почему сразу? Потому что в момент получения ресурса мы точно знаем, что он был взят. Дальше в функции начинаются проверки, условия и возвраты, и вероятность забыть об освобождении резко возрастает. Если же defer
ставится сразу, уборка гарантируется автоматически, и об этом больше не приходится заботиться.
Как ставить defer
Синтаксис очень простой: пишешь defer
перед вызовом функции.
defer someFunc()
Где someFunc
— это та функция, которая освобождает ресурс.
Например:
file, err := os.Open("data.txt")
if err != nil {
return err
}
defer file.Close() // файл точно закроется при выходе из функции
resp, err := http.Get("https://example.com")
if err != nil {
return err
}
defer resp.Body.Close() // освободим соединение
mu.Lock()
defer mu.Unlock() // мьютекс точно разлочится
Важный момент
defer
всегда сработает в конце текущей функции, даже если выйдешь через return
или произойдёт ошибка. Поэтому его ставят в том месте, где ресурс берётся, а не где-то в конце.
Два правила defer
Аргументы фиксируются сразу. Если в defer
передать переменную, её значение берётся в тот же момент. Все изменения дальше уже не повлияют.
x := 10
defer fmt.Println(x) // запомнится 10
x = 20
// В конце функции выведется 10
Если нужно вывести последнее значение, используют функцию-замыкание:
defer func() {
fmt.Println(x) // тут выведется 20
}()
Выполняются в обратном порядке. Если в функции несколько defer
, они будут вызваны в порядке «последний объявлен — первый выполнен».
defer fmt.Println("первый")
defer fmt.Println("второй")
defer fmt.Println("третий")
Результат:
третий
второй
первый
Это удобно и логично: если открыть ресурс А, потом ресурс B, то закрываться они должны в обратном порядке — сначала B, потом А.
defer
— это страховка от человеческой невнимательности. Он гарантирует, что все открытые ресурсы будут закрыты в нужный момент: база — освободит соединение, HTTP — отдаст сокет обратно, файл — закроет дескриптор. Это работает одинаково для любых подобных задач и избавляет от классовых багов, связанных с забытым Close()
.
Самостоятельная работа
Закрепим на практике работу с defer
классической задачей: будем читать файл и сразу закрывать ресурс.
Реализуйте функцию printFile
, которая открывает текстовый файл, читает из него данные и печатает их на экран. Используйте defer
, чтобы гарантировать закрытие файла, даже если во время чтения произойдёт ошибка.
Подсказки:
- Используйте
os.Open
для открытия файла иdefer file.Close()
сразу после успешного открытия. - Для чтения можно воспользоваться
io.ReadAll
. - Обрабатывайте возможные ошибки и выводите их через
fmt.Println
.
Эталонное решение
package main
import (
"fmt"
"io"
"os"
)
func printFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
return err
}
fmt.Print(string(data))
return nil
}
func main() {
if err := printFile("data.txt"); err != nil {
fmt.Println("error:", err)
}
}
Дополнительные материалы
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.