Перейти до змісту

defer

defer планує виклик функції на момент повернення з обгортаючої функції. Саме так у Go виконується cleanup (прибирання ресурсів): закриття файлів, зняття блокувань мʼютексів, завершення HTTP-відповідей, зупинка таймерів.

Базова форма

func read(path string) ([]byte, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer f.Close()           // виконується при поверненні з read
    return io.ReadAll(f)
}

Рядок defer f.Close() гарантує, що f.Close() буде викликано незалежно від того, як завершиться read — звичайне повернення, раннє повернення чи паніка. Більше не потрібно памʼятати «закрий f у кожній точці виходу».

З досвіду Python: defer приблизно відповідає тому, що робить try/finally або оператор with — прив'язує прибирання до виходу з поточної області видимості. Відмінність: defer стосується функції, а не блоку.

Три семантичні правила

1. Порядок LIFO

Відкладені виклики виконуються у зворотному порядку відносно їх планування.

func main() {
    defer fmt.Println("third")
    defer fmt.Println("second")
    defer fmt.Println("first")
    fmt.Println("main")
}
// output:
// main
// first
// second
// third

Уявіть стек: кожен defer кладе елемент; при поверненні всі вони знімаються.

2. Аргументи обчислюються у момент defer

Це спантеличує кожного рівно один раз.

func main() {
    x := 1
    defer fmt.Println(x)      // x захоплюється як 1 прямо зараз
    x = 2
}
// output: 1

Аргумент x обчислюється в момент виконання оператора defer, а не тоді, коли функція врешті-решт буде викликана. Якщо потрібно захопити поточне значення на момент повернення, відкладайте замикання (closure):

func main() {
    x := 1
    defer func() {
        fmt.Println(x)        // замикається над x — зчитується у момент виклику
    }()
    x = 2
}
// output: 2

3. Виконується при будь-якому поверненні — включно з панікою

panic — це механізм Go для ситуацій «такого не повинно бути» — він зупиняє функцію та починає розкручувати стек, доки або recover не перехопить паніку, або програма не завершиться аварійно. Повна розповідь міститься в panic та recover; важливий для defer момент: відкладені виклики все одно виконуються під час розкручування.

func cleanup() {
    defer fmt.Println("running cleanup")
    panic("boom")
}
// output:
// running cleanup
// panic: boom
//   ... stack trace ...

Саме тому defer — природне місце для викликів recover()recover робить щось корисне лише всередині відкладеної функції:

func safe() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("recovered from:", r)
        }
    }()
    panic("oops")
}

4. Відкладене замикання може змінити іменоване значення, що повертається

Відкладена функція виконується після того, як інструкція return задала результат, але перед тим, як його отримає викликач. Якщо результати іменовані, відкладене замикання може їх прочитати й змінити — це механізм, що стоїть за патерном «паніка в помилку».

func double(x int) (result int) {
    defer func() { result *= 2 }()
    result = x + 1
    return result            // задає result = 5, потім defer його подвоює
}

fmt.Println(double(4))       // output: 10

return result зберігає 5 у result; далі виконується відкладене замикання й подвоює його до 10 — і саме 10 бачить викликач.

Ідіоматичне використання

Закриття файлу

f, err := os.Open(path)
if err != nil {
    return err
}
defer f.Close()

Зняття блокування мʼютексу

sync.Mutex — це мʼютекс взаємного виключення зі стандартної бібліотеки — детально розглядається в темі конкурентності. Патерн нижче є канонічним використанням defer: взяти блокування, одразу запланувати його зняття, а потім виконати будь-яку критичну роботу, яку захищає блокування.

var mu sync.Mutex

func update() {
    mu.Lock()
    defer mu.Unlock()
    // ... критична секція ...
}

Відновлення стану

func quiet() func() {
    oldLevel := log.Default().Flags()
    log.Default().SetFlags(0)
    return func() {
        log.Default().SetFlags(oldLevel)
    }
}

func main() {
    defer quiet()()           // зверніть увагу на подвійні () — quiet повертає функцію cleanup
    log.Println("hello")
}

Вимірювання часу виконання функції

func track(name string) func() {
    start := time.Now()
    return func() {
        fmt.Printf("%s took %v\n", name, time.Since(start))
    }
}

func work() {
    defer track("work")()
    time.Sleep(100 * time.Millisecond)
}
// output: work took 100.xxx ms

Підводні камені

defer у циклі накопичується

defer виконується при поверненні з функції, а не після ітерації циклу:

func processAll(paths []string) error {
    for _, p := range paths {
        f, err := os.Open(p)
        if err != nil {
            return err
        }
        defer f.Close()       // !!! всі файли залишаються відкритими до повернення з processAll
        // ... робимо щось ...
    }
    return nil
}

Виправлення: виокремте роботу в окрему функцію, щоб defer виконувався на кожен виклик:

func processAll(paths []string) error {
    for _, p := range paths {
        if err := processOne(p); err != nil {
            return err
        }
    }
    return nil
}

func processOne(p string) error {
    f, err := os.Open(p)
    if err != nil {
        return err
    }
    defer f.Close()           // закривається на кожній ітерації
    // ... робимо щось ...
    return nil
}

defer має мізерну вартість

Вимірюється в наносекундах. У звичайному коді не варто про це думати. У гарячому внутрішньому циклі (мільйони викликів/с) можна вбудувати cleanup вручну.

Не відкладайте повернення до перевірки помилки

f, err := os.Open(path)
defer f.Close()               // !!! паніка, якщо err != nil і f дорівнює nil
if err != nil { ... }

Завжди спершу перевіряйте err, потім defer:

f, err := os.Open(path)
if err != nil {
    return err
}
defer f.Close()

Джерела