Ошибки и defer

defer: порядок выполнения и отличие ожидаемой ошибки от panic

Содержание курса

В прошлом уроке функции начали возвращать error как второе значение, а вызывающий код проверял if err != nil. Это позволило явно обрабатывать предсказуемые сбои. Сейчас добавим к этому ещё один инструмент — defer, — а потом разберёмся, что такое panic и почему это совсем другое.

defer: регистрация отложенного вызова и порядок LIFO

Когда Go встречает строку defer f(), он не вызывает f немедленно. Он запоминает вызов и откладывает его до момента выхода из текущей функции — через return, через достижение конца тела, или из-за аварии (panic). Аргументы при этом вычисляются сразу, в момент регистрации, а не в момент выполнения.

Посмотрим на простой пример:

package main

import "fmt"

func countdown() {
    defer fmt.Println("три")
    defer fmt.Println("два")
    defer fmt.Println("один")
    fmt.Println("пуск")
}

func main() {
    countdown()
}

Вывод:

пуск
один
два
три

Go накапливает зарегистрированные defer в стек. Когда функция завершается, этот стек раскручивается: последний добавленный вызов выполняется первым. Отсюда LIFO — Last In, First Out.

Почему именно стек? Если в одной функции несколько defer, то порядок их выполнения зеркален порядку регистрации. В примере выше "один" зарегистрирован последним — он и выводится первым, затем "два", затем "три". Это предсказуемо и не зависит от того, через какой именно return вышла функция.

Важный нюанс с аргументами:

func show() {
    x := 1
    defer fmt.Println("x при регистрации:", x) // запомнит значение 1
    x = 99
    fmt.Println("x сейчас:", x)
}

Вывод:

x сейчас: 99
x при регистрации: 1

Значение x было вычислено и сохранено в момент выполнения строки defer, а не в момент выхода из функции. Последующее изменение x на 99 уже не влияет на то, что выведет отложенный вызов.