Go: Функции

Теория: defer

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

Рекомендуемые программы

+7 800 100 22 47

бесплатно по РФ

+7 495 085 21 62

бесплатно по Москве

108813 г. Москва, вн.тер.г. поселение Московский,
г. Московский, ул. Солнечная, д. 3А, стр. 1, помещ. 20Б/3
ОГРН 1217300010476
ИНН 7325174845