Недавно столкнулся с такой проблемой, что не всегда приходиться выполнять defer вызов функции. Те кто знают, как работает defer можете листать вниз до реализации моего паттерна.
Представьте, что у вас есть 10 кейсов, в одном из которых не вам не нужен вызов defer func. Что же тогда делать......
Оператор defer помещает вызов функции в список. Список сохраненных вызовов выполняется после того, как возвращается функция. Defer обычно используется для упрощения функций, выполняющих различные действия по очистке.
package main
import "fmt"
func foo(){
defer fmt.Println("Deffered out")
fmt.Println("End of func")
}
func main(){
foo()
}
Результат:
End of func
Deffered out
После выполнения функции и вывода "End of func" функция завершается и вызывается defer func. По итогу, имеем такой вывод.
Рассмотрим примеры из официальной документации (https://go.dev/blog/defer-panic-and-recover).
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
dst, err := os.Create(dstName)
if err != nil {
return
}
written, err = io.Copy(dst, src)
dst.Close()
src.Close()
return
}
Это работает, но есть ошибка. Если вызов os.Create не удался, функция вернется, не закрыв исходный файл. Это можно легко исправить, поместив вызов src.Close перед вторым оператором возврата, но если бы функция была более сложной, проблему было бы не так легко заметить и решить. Используя defer, мы можем гарантировать, что файлы всегда будут закрыты.
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
defer src.Close()
dst, err := os.Create(dstName)
if err != nil {
return
}
defer dst.Close()
return io.Copy(dst, src)
}
defer гарантирует, что, независимо от количества возврата в функции, файлы будут закрыты.
Работа defer осуществляется по трём правилам:
Аргументы defer функции передаются на этапе создания defer call.
defer функции вызываются в порядке Last In First Out после завершения внешней функции.
defer функция умеет работать с возвращаемым значением по умолчанию функции.
Рассмотрим примеры кода для каждого пункта. Передача аргументов в defer функцию:
package main
import "fmt"
func foo() {
a := 10
defer fmt.Println(a)
a += 20
fmt.Println("End of func")
}
func main(){
foo()
}
По завершении работы функции foo() наблюдается вот такой вывод:
End of func
10
Вызов defer функций в порядке LIFO:
package main
import "fmt"
func foo() {
for i := 0; i < 4; i++ {
defer fmt.Print(i)
}
}
func main(){
foo()
}
Результат:
3210
Последняя вызванная defer функция выполняется первой (LIFO)
defer функция умеет работать с возвращаемым значением по умолчанию функции:
package main
import "fmt"
func c() (i int) {
defer func() { i += 100 }()
return 100
}
func main() {
fmt.Println(c())
}
Функция возвращает i = 100, потом defer функция увеличивает i на 100, отсюда вывод:
200
defer с возможностью отмены его вызова
Удобство использования defer никто не отрицает, но что если в одном из кейсов отсутствует надобность вызова defer функции. Тогда можно применить следующую конструкцию
package main
import "fmt"
func foo(a int){
var Execute *bool = new(bool)
*Execute = true
defer fmt.Println("End of func")
defer func(ex *bool) {
if *ex {
fmt.Println(a / 2)
}
}(Execute)
switch{
case a % 2 == 0:
fmt.Printf("%d is even\n", a)
return
case a % 2 == 1:
fmt.Printf("%d is odd\n", a)
*Execute = false
return
default:
*Execute = false
return
}
}
func main(){
foo(4)
fmt.Println()
foo(5)
}
Так как аргументы передаются на момент создания defer функция, будем передавать указатель на bool, чтобы проверять нужно ли нам вызывать функция в defer. На вход поступает 4, оно чётное, поэтому после return вызывается defer функция, в которой проверяется Execute, поэтому выводится a / 2.
На вход поступает цифра 5, оно нечётная, поэтому в case, разыменовывая указатель, изменяется значение Execute. Следовательно, после return в defer функции мы не выводим нашу цифру, поделённую пополам, так как *Execute = false.
4 is even
2
End of func
5 is odd
End of func
Комментарии (12)
k0nart
26.12.2024 18:38Кликбейт какой-то.
Причем тут отмена defer, если он совершенно успешно выполнился?
И зачем эти танцы с указателем? Вы же понимаете, что можно убрать Execute из кода, в defer убрать аргументы и просто там проверятьif a%2 == 0 { fmt.Println(a / 2) }
Только для очень условного внешнего управления? Да, можно, но это, как выше заметили очень искусственный пример. В реальной жизнь это буквально говорит о том, что нужно код переписать.
Sly_tom_cat
26.12.2024 18:38Там собственно и defer не нужен - если есть только одна ветка где нужно делить - там и делить.
k0nart
26.12.2024 18:38Ну и отдельно напишу, раз уж про переписывание упомянул, что тут что-то типа декоратора напрашивается, а не абьюз defer (не используйте defer, пожалуйста, если он вам не нужен),
func dec(f func(int), i int) {
if i%2 == 0 {
f(i)
}
}
или что-то типа того
SkilledOne
26.12.2024 18:38Я извиняюсь за негатив, вообще не часто (никогда) пишу комментарии, но что это за статья ради статьи? Автор серьёзно использовал 1 if и написал об этом статью? При всем при этом зачем-то используется указатель на локальную переменную И анонимная функция, из которой можно получить прямой доступ к переменной, а не по ссылке. Для кого эта статья? (Ещё раз извиняюсь за негатив, но это уже совсем ни в какие рамки не лезет)
SpaghettiRebel
26.12.2024 18:38Очень занятная статья на самом деле. Вскрывает всю подноготную нашего нелёгкого ремесла, особенно в такие времена. Прочитал с удовольствием, публикация — настоящая отдушина для разбирающегося в своём деле человека.
По этому поводу вспомнился один очень хороший советский анекдот, услышанный мной на радиорынке в Урюпинске году этак в 89-м.
Запомните раз и навсегда, что шуруп, забитый молотком, всегда лучше, чем гвоздь, завернутый отверткой. - золотые слова. Особенно очевидно это становится, когда приходит опыт в профессиональной деятельности. Для кого эта статья? Мне кажется – для искушенных профессионалов, горящим своим делом. Спасибо, что поделились!
sl4mmer
26.12.2024 18:38Ситуация очень искуственная, но я плюсанул и карму и статью - все таки хочется видеть побольше на хабре инженерных идей (пусть даже сырых и не особо продуманых), а не бесконечные блоги компаний
Sly_tom_cat
Ну для начала - это никакая не отмена defer, правильнее было бы назвать это отключением.
Во, вторых предложенный пример уж очень сильно искусственный (создаем странную логику и старательно с ней боремся).
У вас даже switch - не к месту ибо int всегда либо четный, либо нет: достаточно одного if else или if return без else.
Ну да, зануда. А что поделать если
говнокодне оптимальные решения режут глаз...tolyanski
Я тоже, увидев заголовок статьи, ожидал какой-то магический механизм. Какой-нить сахар типа cancel defer, о котором я не знал... А тут получилось первая часть статьи вообще не по теме заголовка, а во второй автор предлагает обычные костыли... для кейса который в реальной жизни и так редко нужен, если руки прямые)
Kpatoc452 Автор
Спасибо за отзыв. Да, согласен, ситуация в вакууме. Идею придумал за вечер. Крутое решением, мб, через RSP регистр спуститься вниз по стэку и убрать вызов defer функции. Буду стараться, первый блин очень большим комом