Аргументы defer и именованный результат
Замер времени в логах «Полки» показывал почти ноль, а в отчёт уходили старые данные. Миша нашёл причину: уборка захватывала значения в момент объявления, до того как они изменились.
аргументы отложенного вызова вычисляются в момент объявления defer, а замыкание видит переменную живой — и может изменить именованный результат.
Чаще всего людей сбивают с толку две вещи: порядок выполнения (о нём — прошлый урок) и момент вычисления аргументов.
Аргументы вычисляются сразу
Что здесь показано
- Стек — локальные переменные функции.
i = 3— значение;p = ptr → 10— указатель на значение;b = "hello"— срез (заголовок: указатель + длина + ёмкость). - Куча — данные, на которые ссылаются переменные:
rodata— байты строки-литерала (в бинарнике),данные N Б— бэкинг-массив слайса,строка— динамическая строка. - Горутины — лёгкие потоки Go: статус (running/runnable/waiting…), функция, в которой каждая сейчас находится, размер стека (растёт по мере работы) и пометка «из main» — горутина запущена из твоего кода.
- GC — сборщик мусора: фаза (off/mark) и число завершённых циклов.
fmt.Println(i) увидел i равной нулю и именно ноль напечатает на выходе, хотя к тому моменту переменная уже единица. Ловит это людей в реалистичных сценариях, самый частый из которых — замер времени:
t := time.Now()
defer log.Println(time.Since(t)) // неправильно: аргумент посчитается сразу
Кажется, что time.Since(t) посчитается на выходе. На деле аргумент вычисляется при defer, поэтому в лог уйдёт почти ноль. Именно так замер длительности операции в «Полке» писал в лог бессмысленные нули. Правильно — обернуть в замыкание:
defer func() { log.Println(time.Since(t)) }()
Замыкание видит живую переменную
Что здесь показано
- Стек — локальные переменные функции.
i = 3— значение;p = ptr → 10— указатель на значение;b = "hello"— срез (заголовок: указатель + длина + ёмкость). - Куча — данные, на которые ссылаются переменные:
rodata— байты строки-литерала (в бинарнике),данные N Б— бэкинг-массив слайса,строка— динамическая строка. - Горутины — лёгкие потоки Go: статус (running/runnable/waiting…), функция, в которой каждая сейчас находится, размер стека (растёт по мере работы) и пометка «из main» — горутина запущена из твоего кода.
- GC — сборщик мусора: фаза (off/mark) и число завершённых циклов.
Тело замыкания захватывает переменную i и читает её в момент выполнения, уже после присваивания. Противоречия с примером выше нет: defer fmt.Println(i) скопировал значение аргумента, а замыкание работает с живой переменной.
Именованный результат
defer может менять возвращаемое значение — но только именованное:
func count() (n int) {
defer func() { n++ }()
return 2
}
fmt.Println(count()) // 3
return 2 присваивает n = 2, потом выполняются отложенные вызовы, один из них делает n++, и уже после этого функция отдаёт наружу итоговое n = 3. С анонимным результатом отложенной функции не за что было бы зацепиться.
Та же логика у получателя метода: defer c.log снимает копию c в момент объявления, и метод с value-receiver отработает именно с ней.
Вы узнаете:
- почему
recoverживёт именно вdefer - какие накладные расходы у defer в горячем цикле
Проверьте себя
i := 0; defer fmt.Println(i); i = 1. Что напечатается?
defer c.log() с value-receiver, затем c.n = 2. Что напечатает log?
Почему defer log.Println(time.Since(t)) пишет в лог почти ноль?
правильных ответов
Аргументы defer фиксируются сразу; замыкание видит переменную в момент выполнения; именованный результат defer может подправить. Три правила, на которых держится почти вся путаница с defer.
10 000 ₽ · 60 минут · онлайн