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 не перехопить паніку, або програма не завершиться аварійно. Повна розповідь міститься в 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 бачить викликач.
Ідіоматичне використання¶
Закриття файлу¶
Зняття блокування мʼютексу¶
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: