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¶
Це спантеличує кожного рівно один раз.
Аргумент x обчислюється в момент виконання оператора defer, а не тоді, коли функція врешті-решт буде викликана. Якщо потрібно захопити поточне значення на момент повернення, відкладайте замикання (closure):
func main() {
x := 1
defer func() {
fmt.Println(x) // замикається над x — зчитується у момент виклику
}()
x = 2
}
// output: 2
3. Виконується при будь-якому поверненні — включно з панікою¶
panic — це механізм Go для ситуацій «такого не повинно бути» — він зупиняє функцію та починає розкручувати стек, доки або recover не перехопить паніку, або програма не завершиться аварійно. Повна розповідь міститься в 12-panic-and-recover.md; важливий для 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")
}
Ідіоматичне використання¶
Закриття файлу¶
Зняття блокування мʼютексу¶
sync.Mutex — це мʼютекс взаємного виключення зі стандартної бібліотеки — детально розглядається в темі конкурентності. Патерн нижче є канонічним використанням defer: взяти блокування, одразу запланувати його зняття, а потім виконати будь-яку критичну роботу, яку захищає блокування.
Відновлення стану¶
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: