← Назад к вопросам

Что выведет код? defer и значения аргументов

1.8 Middle🔥 111 комментариев
#Основы Go

Условие

Определите, что выведет следующий код:

package main

import "fmt"

func main() {
    x := 1
    defer fmt.Println("defer 1:", x)
    x++
    defer fmt.Println("defer 2:", x)
    x++
    fmt.Println("main:", x)
}

Вопросы

  1. Что выведет программа?
  2. В какой момент вычисляются аргументы defer?
  3. Как изменить код, чтобы defer выводил актуальное значение x?

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

defer и значения аргументов - полное решение

Ответы на вопросы

1. Что выведет программа?

main: 3
defer 2: 2
defer 1: 1

Объяснение порядка вывода:

  • main: 3 - выводится в процессе выполнения main
  • defer 2: 2 - выводится при выходе из main в обратном порядке регистрации (LIFO)
  • defer 1: 1 - самый последний зарегистрированный defer

2. В какой момент вычисляются аргументы defer?

Аргументы defer вычисляются в момент регистрации defer, а не в момент его выполнения.

Подробный анализ выполнения

package main

import "fmt"

func main() {
    x := 1
    // Момент 1: регистрация defer 1
    // Аргумент fmt.Println вычисляется СЕЙЧАС
    // x имеет значение 1
    defer fmt.Println("defer 1:", x)
    
    x++  // x становится 2
    
    // Момент 2: регистрация defer 2
    // Аргумент fmt.Println вычисляется СЕЙЧАС
    // x имеет значение 2
    defer fmt.Println("defer 2:", x)
    
    x++  // x становится 3
    
    // Момент 3: обычный вывод
    fmt.Println("main:", x)  // x имеет значение 3
    
    // Момент 4: выход из функции
    // defer выполняются в ОБРАТНОМ порядке (LIFO)
    // defer 2 выводит сохраненное значение 2
    // defer 1 выводит сохраненное значение 1
}

Стек defer (внутренний механизм)

Время выполнения:

Шаг 1: x=1, defer fmt.Println("defer 1:", 1)
  Стек: [defer1(1)]

Шаг 2: x=2, defer fmt.Println("defer 2:", 2)
  Стек: [defer1(1), defer2(2)]  <- новые добавляются в конец

Шаг 3: fmt.Println("main:", 3)
  Вывод: "main: 3"

Шаг 4: выход из main, стек выполняется в обратном порядке
  Выполняем defer2(2) -> "defer 2: 2"
  Стек: [defer1(1)]
  
  Выполняем defer1(1) -> "defer 1: 1"
  Стек: []

Ключевое правило

"Аргументы defer вычисляются в момент регистрации, но функция выполняется при выходе"

Это отличается от ожидания многих программистов, которые думают, что defer работает как lambda с отложенным вычислением.

Вариант с передачей указателя

Если нужно, чтобы defer выводил актуальное значение, используйте указатель:

func main() {
    x := 1
    defer func() { fmt.Println("defer 1:", x) }()  // вычисляется при выполнении
    x++
    defer func() { fmt.Println("defer 2:", x) }()  // вычисляется при выполнении
    x++
    fmt.Println("main:", x)
}

// Вывод:
// main: 3
// defer 2: 3
// defer 1: 3

Почему это работает?

  • Defer регистрирует анонимную функцию
  • Функция захватывает переменную x по ссылке (closure)
  • Переменная x вычисляется в момент выполнения функции

Альтернатива с параметром функции

func main() {
    x := 1
    defer func(val int) { fmt.Println("defer 1:", val) }(x)
    x++
    defer func(val int) { fmt.Println("defer 2:", val) }(x)
    x++
    fmt.Println("main:", x)
}

// Вывод:
// main: 3
// defer 2: 2
// defer 1: 1

Здесь аргумент функции вычисляется в момент defer, поэтому получаем старые значения.

Практические примеры

Проблема: неправильное закрытие файла

// ❌ ПЛОХО - файл может не закрыться
for i := 1; i <= 3; i++ {
    file, _ := os.Open(fmt.Sprintf("file%d.txt", i))
    defer file.Close()  // все три defer указывают на ПОСЛЕДНИЙ файл
}

Решение: используйте функцию

// ✓ ХОРОШО - каждый файл закрывается правильно
for i := 1; i <= 3; i++ {
    file, _ := os.Open(fmt.Sprintf("file%d.txt", i))
    defer func(f *os.File) { f.Close() }(file)  // передаем текущий файл
}

// Или лучше с IIFE:
for i := 1; i <= 3; i++ {
    func() {
        file, _ := os.Open(fmt.Sprintf("file%d.txt", i))
        defer file.Close()  // теперь правильно для каждого i
    }()
}

Пример с panic recovery

func main() {
    x := 1
    defer func() { 
        fmt.Println("Финальное x:", x)  // выведет 999
    }()
    
    x = 999
    panic("ошибка")
}

// Вывод:
// Финальное x: 999
// panic: ошибка

Важно: defer все равно выполняется при panic!

Сложность и производительность

  • Вычисление аргументов: O(1) - просто вычисляют значения
  • Добавление в стек defer: O(1)
  • Выполнение при выходе: O(n) где n - количество defer

Best Practices

  1. Помните о замыкании (closure) - defer захватывает переменные, не их значения
  2. Используйте функции для отложенного вычисления - если нужны актуальные значения
  3. Остерегайтесь циклов - убедитесь что каждая итерация правильно захватывает переменные
  4. defer для cleanup - используйте для закрытия файлов, разблокировки mutex, откатов
  5. Порядок важен - defer выполняются в обратном порядке регистрации