О себе
Привет! Я Артур Давыдов, бэкенд разработчик на Go. В этой статье хочу рассмотреть поведение defer более детально. Надеюсь, что статья будет полезна.

Введение
Defer это мощный инструмент в Go. Его можно (с огромной натяжкой) сравнить с деструкторами С++ или Finalizer в Dart, но происходит все действо в пределах стека одной функции. И этих вызовов может быть несколько
Это база
Defer в Go перемещает вызов функции в стэк (LIFO очередь) отложенных вызовов. Другими словами, функции в defer будут завершены при закрытии стека основной, относительно запуска defer, функции.
Отложенный запуск позволяет нам позаботиться о закрытии соединений, передачи информации при закрытии функции, отлавливать паники через recover и т.д.
Важно понимать, что defer позволяет выполнить что угодно в рамках функции и не требует только закрывать соединения.
Конструкции c defer встречаются в Go настолько же часто (субъективное мнение и наблюдения автора), как и набившие оскомину if err != nil.
func AbstractFunction() error { // Тут могло быть ваше соединение с чем угодно f, err := os.OpenFile("zdravcity.txt", os.O_RDWR, 0777) if err != nil { return fmt.Errorf("Abstract function(Open file): %w", err) } defer f.Close() /* Какая-то логика. Тут файл еще открыт. */ return nil }
Анонимка или в одну строчку?
Конечно, этот вопрос не касается defer непосредственно, однако, тут можно споткнуться особенно на собесах.
Что такое анонимная функция в Go?
Анонимная функция в Golang — это функция, у которой нет имени. Ее можно определить непосредственно там, где она нужна, без объявления отдельной именованной функции
Давайте вспомним, в Go все передается по значению. Однако, есть разница копия структуры или указателя на нее, о чем мы убедимся по ходу статьи.
Для вызова функции, в defer помещается указатель на функцию.
Что бы убедиться, что у функций есть указатель, можете запустить этот код. Результатом его работы должен быть адрес анонимной функции напечатанный в терминале.
func main() { fmt.Printf("0x%x\n", reflect.ValueOf(func() {}).Pointer()) // Даже у такой функции есть указатель }
А что будет, если передавать функцию с аргументами?
Go подставит значения переменных в вызываемую функцию и отложит ее выполнение на завершение основной функции (относительно defer).
Таким образом, вы спокойно можете понять разницу в этих двух defer и что они выведут.
func main() { n := 0 defer fmt.Println(n) // 0, тут мы сразу передали в печать значение 0 defer func(n int) { fmt.Println(n) // 0. Пусть вас не смущает, что печатается n. Этот n является аргументом анонимки, а не глобальный // Это эффект затенения, когда переменная затеняет собой одноименную, но уровнем видимости выше }(n) // Этот дефер обернут в анонимку, однако, n передается сразу на инициализации defer func() { fmt.Println(n) // 2, т.к. анонимка просчитает себя только в момент вызова }() n++ // n = 1 <---- Начинай читать код тут defer func() { n++ // n = 2 <----- Первым будет выполнен этот defer, т.к. по LIFO он "наверху" стакана, а следом пойдут вышестоящие, тут без подвоха (?) }() }
Картина меняется сразу, если мы начнем передавать не значение n, а указатель.
func main() { zero := 0 n := &zero // Отработает последним defer fmt.Println(*n) // 0, а тут не смотря на указатель, мы его разыменовали сразу и передали значение // Отработает третьим defer fmt.Println(n) //0xc000010120. Но если убрать разыменоание, во всех деферах будет один указатель // Отработает вторым defer func(n *int) { fmt.Println(*n) // 2 fmt.Println(n) // 0xc000010120 }(n) // Отработает первым defer func() { fmt.Println(*n) // 2 fmt.Println(n) // 0xc000010120 }() *n++ // n = 1 <---- Начинай читать код тут defer func() { *n++ // n = 2 }() }
И ответ на заголовок может показаться банальным, но анонимка или инлайн, все зависит от ваших целей. Хотите зафиксировать defer`ом состояние переменной в какой-то момент времени? Или хотите работать с "конечным" результатом? Тут речь не о идиоматике языка, а скорее о его возможностях.
Жизнь defer после return
Когда вы будете читать о defer, в материалах укажут, что он выполняется в конце работы функции. Как правило, на этом все и заканчивается. Более того, в работе вы можете и не заметить нюанса, а именно, defer отрабатывает ПОСЛЕ отработки return, но до непосредственного схлопывания стека.
Проверить это достаточно просто.
func Increment(n int) int { defer func() { n++ // Инкремент будет выполнен после копирования n в возврат }() return n // 1 } func main() { fmt.Println(Increment(1)) // 1. Инкремента нет }
Вы скажете, что произошло копирование, как в примере выше. Достаточно на return накинуть анонимку, что б выполнить инкремент и передать его результат. Проверяем
func Increment(n int) int { defer func() { n++ }() return func() int { return n // 1 }() } func main() { fmt.Println(Increment(1)) }

Тут, конечно, помогут нам указатели. Т.к. возврат указателя не фиксирует данные, на которые он показывает. Мы все равно можем менять эти данные. И они будут видны при последующем разыменовании
func Increment(n *int) *int { defer func() { *n++ }() return n // Возвращаем именно указатель, а не его копию } func main() { n := 1 fmt.Println(*Increment(&n)) //2. Разменуем }
Вы можете сказать, что это примеры могут появиться только на тех интервью, да и то не факт, что их зададут. А вот и нет
Когда это может выстрелить в работе и как бороться?
У вас в структуре есть мьютекс и он покрывает >1 критической секции? Метод открытый для внешнего вызова сам вызывает через return портянку мелких методов с мьютексом?
type Example struct { m sync.Mutex } func (e *Example) Global() error { e.m.Lock() defer e.m.Unlock() return e.proxy() } func (e *Example) proxy() error { return e.local() } func (e *Example) local() error { e.m.Lock() // А мьютекс выше еще закрыт, а мы уже тут, а еще мы в main :) defer e.m.Unlock() return nil } func main() { n := &Example{} fmt.Println(n.Global()) }

Как починить это? Ответ простой и анонимный.
Мы заключим работу мьютекса в анонимную функцию, что б defer отработал внутри нее и global пошла дальше, с открытым мьютекcом
type Example struct { m sync.Mutex } func (e *Example) Global() error { func() { e.m.Lock() defer e.m.Unlock() }() return e.proxy() } func (e *Example) proxy() error { return e.local() } func (e *Example) local() error { e.m.Lock() // А мьютекс выше еще закрыт, а мы уже тут, а еще мы в main :) defer e.m.Unlock() return nil } func main() { n := &Example{} fmt.Println(n.Global()) }

А что если обернуть это все дело в горутину, без использования анонимной функции?
type Example struct { m sync.Mutex } func (e *Example) Global() error { e.m.Lock() defer e.m.Unlock() return e.proxy() } func (e *Example) proxy() error { return e.local() } func (e *Example) local() error { e.m.Lock() // А мьютекс выше еще закрыт, а мы уже тут :) defer e.m.Unlock() return nil } func main() { n := &Example{} ch := make(chan struct{}) go func() { defer func() { ch <- struct{}{} }() fmt.Println(n.Global()) }() <-ch }

Тут блокировка происходит, как вышестоящие, т.к. рантайм Go понимает, что выхода можно не ждать, все горутины заблокированы. Следуя этому правилу, мы можем доработать код и он не упадет. Достаточно выключить в него еще одну горутину, которая запишет в тот же канал хоть что-то
// Нужно добавить рядом с горутиной, вызывающей Global go func() { defer func() { ch <- struct{}{} }() time.Sleep(time.Second * 10) }()
Это решение работает. Программа завершит работу без ошибок и паник через 10 секунд, однако, мы потеряли выполнение целого куска Global, который был вызван. Обойти это можно только прибегая к закрыванию defer`a в Global в анонимную функцию
type Example struct { m sync.Mutex } func (e *Example) Global() error { func() { e.m.Lock() defer e.m.Unlock() }() return e.proxy() } func (e *Example) proxy() error { return e.local() } func (e *Example) local() error { e.m.Lock() // А мьютекс выше еще закрыт, а мы уже тут :) defer e.m.Unlock() return nil } func main() { n := &Example{} ch := make(chan struct{}) go func() { defer func() { ch <- struct{}{} }() fmt.Println(n.Global()) }() go func() { defer func() { ch <- struct{}{} }() time.Sleep(time.Second * 10) }() <-ch }

А может ли defer затормозить выполнение кода?
Да. В качестве примера предположим, что у нас есть канал в структуре. У структуры есть читатель и писатель канала. Задача писателя записать в канал структуру, как только произойдет сложная операция, причем, без разницы, с каким результатом
type Example struct { m sync.Mutex ch chan struct{} t time.Time } func (e *Example) A() { time.Sleep(time.Second) // Например, вы вставляете большой объем данных defer func() { // Хотим, что б читатель приступил к работе, как только вставка закончится, либо произойдет какая-то иная беда e.ch <- struct{}{} }() // Имитация очень тяжелой задачи после вставки time.Sleep(time.Second * 10) } func (e *Example) B() { <-e.ch fmt.Printf("Script ended in: %.2f seconds\n", time.Since(e.t).Seconds()) } func main() { n := &Example{ ch: make(chan struct{}), t: time.Now(), } wg := sync.WaitGroup{} wg.Add(2) go func() { defer func() { wg.Done() }() n.A() }() // Эта горутина никогда не завершит работу, т.к. верхняя запишет в канал раньше go func() { defer func() { wg.Done() }() n.B() }() wg.Wait() }
Этот код отработает за ~11 секунд. Эту ситуацию и тут поможет обойти анонимка.

Вместо вывода
Этой статьей я хотел бы подчеркнуть, что стоит относиться к defer чуть осторожнее, чем к "закрывашке". А если проблема все же возникла, то ее, иногда, можно подлатать простой анонимкой.
P.S.
Эта статья не претендует на какие-то открытия. Это, скорее, желание попробовать, какого это написать статью для Хабра со скромным желанием кому-то помочь. Прошу сильно не пинать :)
Рекламы ТГ и других соц сетей не будет, я тут за идею.
Не болейте <3
