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 уже не влияет на то, что выведет отложенный вызов.
