О себе

Привет! Я Артур Давыдов, бэкенд разработчик на 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

Комментарии (6)


  1. Sly_tom_cat
    01.06.2025 07:38

    И вам не болейте.
    Я так понял это при повышенной температуре такое в голову пришло....
    Я вот большей половины таких, за уши притянутых примеров кривых деферов не видел и представить себе не мог, в каком смутном состоянии разума можно было бы такие ногострелы написать. Теперь понимаю. Понимаю, что с температурой код лучше не писать.


  1. 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()
    }
    


  1. vened
    01.06.2025 07:38

    Мы заключим работу мьютекса в анонимную функцию, что б defer отработал внутри нее и global пошла дальше, с открытым мьютекcом

    Зачем это может быть нужно? Зачем тогда мьютекс? Привычная логика мьютекса здесь, - как можно предполжить, - в том, чтобы весь код, вызываемый упомянутой функцией, работал в эксклюзивном режиме. То, что вызывается силами конструкции return my_func() - это тоже код, работающий в логическом контексте вызывающей функции (ну, перепишем на res := my_func() и т.д.). Ведь defer же не просто так отрабатывает "после return" - такая последовательность выбрана именно для того, чтобы предоставить языковой механизм для отложенного выполнения конструкций, "закрывающих" сразу всё дерево данной функции. Не нужно считать это за недостаток - Go не обязывает использовать defer для всего и сразу.


  1. 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.


    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
      }
      
      

      https://go.dev/play/p/oCpFl6trX_6


    1. aaveter
      01.06.2025 07:38

      Да. Похоже это разница между выводом через return локальной переменной и именованным выводом без return. Так что автор тоже прав.