Зарегистрируйтесь, чтобы продолжить обучение

defer Go: Функции

В реальном коде мы постоянно берём внешние ресурсы: открываем соединения с базой данных, делаем 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)
    }
}

Дополнительные материалы

  1. Effective Go — Defer
  2. Go by Example — Defer

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff