Вы уверены, что defer всегда безопасен?

от автора

О себе

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

Смотрит на defer

Смотрит на 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)) } 
Все равно 1

Все равно 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()) }
deadlock через двойную блокировку мьютекса

deadlock через двойную блокировку мьютекса

Как починить это? Ответ простой и анонимный.
Мы заключим работу мьютекса в анонимную функцию, что б 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


ссылка на оригинал статьи https://habr.com/ru/articles/914642/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *