О себе
Привет! Я Артур Давыдов, бэкенд разработчик на 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
Комментарии (6)
xxxphilinxxx
01.06.2025 07:38Возможно, я что-то не так понял, но починкой я бы это не назвал: да, избавились от дедлока, но вывели из под защиты e.proxy(). Если бы этот метод был безопасен сам по себе, то и мьютексом в Global() его закрывать не было бы смысла. А если небезопасен, то теперь он стал открыт.
func (e *Example) Global() error { func() { e.m.Lock() defer e.m.Unlock() }() return e.proxy() }
Это равнозначно просто стиранию defer. Множество потоков могут получить и тут же вернуть блокировку, а затем дружно толпой пойти в небезопасный e.proxy().
func (e *Example) Global() error { e.m.Lock() e.m.Unlock() return e.proxy() }
vened
01.06.2025 07:38Мы заключим работу мьютекса в анонимную функцию, что б defer отработал внутри нее и global пошла дальше, с открытым мьютекcом
Зачем это может быть нужно? Зачем тогда мьютекс? Привычная логика мьютекса здесь, - как можно предполжить, - в том, чтобы весь код, вызываемый упомянутой функцией, работал в эксклюзивном режиме. То, что вызывается силами конструкции return my_func() - это тоже код, работающий в логическом контексте вызывающей функции (ну, перепишем на res := my_func() и т.д.). Ведь defer же не просто так отрабатывает "после return" - такая последовательность выбрана именно для того, чтобы предоставить языковой механизм для отложенного выполнения конструкций, "закрывающих" сразу всё дерево данной функции. Не нужно считать это за недостаток - Go не обязывает использовать defer для всего и сразу.
peacecoder85
01.06.2025 07:38Более того, в работе вы можете и не заметить нюанса, а именно, defer отрабатывает ПОСЛЕ отработки return, но до непосредственного схлопывания стека
package main import "fmt" func main() { fmt.Println("Hello, 世界", test()) } func test() (n int) { defer func() { n++ }() return 0 }
к
https://go.dev/play/p/3dvqxPuB4F6
Выводит 1.
SabMakc
01.06.2025 07:38Это немного другой прикол с defer - изменение возвращаемых значений:
package main import "fmt" func main() { fmt.Println("Hello, 世界", test()) } func test() (n int) { defer func() { fmt.Println(n) n = 2 }() return 1 }
aaveter
01.06.2025 07:38Да. Похоже это разница между выводом через return локальной переменной и именованным выводом без return. Так что автор тоже прав.
Sly_tom_cat
И вам не болейте.
Я так понял это при повышенной температуре такое в голову пришло....
Я вот большей половины таких, за уши притянутых примеров кривых деферов не видел и представить себе не мог, в каком смутном состоянии разума можно было бы такие ногострелы написать. Теперь понимаю. Понимаю, что с температурой код лучше не писать.