Недавно столкнулся с такой проблемой, что не всегда приходиться выполнять 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 осуществляется по трём правилам:

  1. Аргументы defer функции передаются на этапе создания defer call.

  2. defer функции вызываются в порядке Last In First Out после завершения внешней функции.

  3. 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)


  1. Sly_tom_cat
    26.12.2024 18:38

    Ну для начала - это никакая не отмена defer, правильнее было бы назвать это отключением.

    Во, вторых предложенный пример уж очень сильно искусственный (создаем странную логику и старательно с ней боремся).

    У вас даже switch - не к месту ибо int всегда либо четный, либо нет: достаточно одного if else или if return без else.

    Ну да, зануда. А что поделать если говнокод не оптимальные решения режут глаз...


    1. tolyanski
      26.12.2024 18:38

      Я тоже, увидев заголовок статьи, ожидал какой-то магический механизм. Какой-нить сахар типа cancel defer, о котором я не знал... А тут получилось первая часть статьи вообще не по теме заголовка, а во второй автор предлагает обычные костыли... для кейса который в реальной жизни и так редко нужен, если руки прямые)


    1. Kpatoc452 Автор
      26.12.2024 18:38

      Спасибо за отзыв. Да, согласен, ситуация в вакууме. Идею придумал за вечер. Крутое решением, мб, через RSP регистр спуститься вниз по стэку и убрать вызов defer функции. Буду стараться, первый блин очень большим комом


  1. k0nart
    26.12.2024 18:38

    Кликбейт какой-то.

    Причем тут отмена defer, если он совершенно успешно выполнился?

    И зачем эти танцы с указателем? Вы же понимаете, что можно убрать Execute из кода, в defer убрать аргументы и просто там проверять

    if a%2 == 0 { fmt.Println(a / 2) }

    Только для очень условного внешнего управления? Да, можно, но это, как выше заметили очень искусственный пример. В реальной жизнь это буквально говорит о том, что нужно код переписать.


    1. Sly_tom_cat
      26.12.2024 18:38

      Там собственно и defer не нужен - если есть только одна ветка где нужно делить - там и делить.


    1. k0nart
      26.12.2024 18:38

      Ну и отдельно напишу, раз уж про переписывание упомянул, что тут что-то типа декоратора напрашивается, а не абьюз defer (не используйте defer, пожалуйста, если он вам не нужен),

      func dec(f func(int), i int) {
      if i%2 == 0 {
      f(i)
      }
      }

      или что-то типа того


  1. SkilledOne
    26.12.2024 18:38

    Я извиняюсь за негатив, вообще не часто (никогда) пишу комментарии, но что это за статья ради статьи? Автор серьёзно использовал 1 if и написал об этом статью? При всем при этом зачем-то используется указатель на локальную переменную И анонимная функция, из которой можно получить прямой доступ к переменной, а не по ссылке. Для кого эта статья? (Ещё раз извиняюсь за негатив, но это уже совсем ни в какие рамки не лезет)


  1. SpaghettiRebel
    26.12.2024 18:38

    Очень занятная статья на самом деле. Вскрывает всю подноготную нашего нелёгкого ремесла, особенно в такие времена. Прочитал с удовольствием, публикация — настоящая отдушина для разбирающегося в своём деле человека.

    По этому поводу вспомнился один очень хороший советский анекдот, услышанный мной на радиорынке в Урюпинске году этак в 89-м.
    Запомните раз и навсегда, что шуруп, забитый молотком, всегда лучше, чем гвоздь, завернутый отверткой. - золотые слова. Особенно очевидно это становится, когда приходит опыт в профессиональной деятельности. Для кого эта статья? Мне кажется – для искушенных профессионалов, горящим своим делом. Спасибо, что поделились!


    1. tolyanski
      26.12.2024 18:38

      Комментарий написан ChatGPT? )


      1. SpaghettiRebel
        26.12.2024 18:38

        Ни в коем случае


  1. sl4mmer
    26.12.2024 18:38

    Ситуация очень искуственная, но я плюсанул и карму и статью - все таки хочется видеть побольше на хабре инженерных идей (пусть даже сырых и не особо продуманых), а не бесконечные блоги компаний


    1. tolyanski
      26.12.2024 18:38

      Нужно также больше инженерных статей по перекладыванию Json'ов