Про defer в Golang
Немного теория
В Go ключевое слово defer
используется для откладывания выполнения функции до момента выхода из текущей функции. Очень полезный функционал, когда нужно освободить какие-то ресурсы (закрыть файл, канал, соединение с базой и т.д.)
func example() { fmt.Println("Старт") defer fmt.Println("Отложено") fmt.Println("Завершение") } // На экран будет выведено: // Старт // Завершение // Отложено
Если в функции объявлено несколько defer, то они выполняются по принципу LIFO (last in, first out), последний пришёл, первый ушёл
func example() { defer fmt.Println("Первый") defer fmt.Println("Второй") defer fmt.Println("Третий") } // На экран будет выведено: // Первый // Второй // Третий
Так же нужно понимать, что аргументы для функции объявленой в defer будут вычисляться в момент определения.
func example() { x := 10 defer fmt.Println("Отложенно x:", x) x = 20 } // На экран будет выведено: // Отложенно x: 10
В общем, вот реальный пример использовать defer
func readFile(name string) { file, err := os.Open(name) if err != nil { log.Fatal(err) } defer file.Close() // Файл закроется при выходе из функции // Что-то делаем с файлом... }
Так же defer может использоваться для обработки паники
func safe() { defer func() { if r := recover(); r != nil { fmt.Println("Обработка паники:", r) } }() panic("Что-то плохое случилось") }
Внутреннее устройство defer
Когда компилятор встречает ключевое слово defer, он создаёт специальную структуру _defer
расположенную в runtime/runtime2.go
в структуре g
. По-простому говоря, в горутине есть стек отложенных вызовов.
g
- это по факту внутреннее представление горутины в рантайме. Про структуру g
подробней разберём в следующих статьях.
type g struct { // Много разных полей... _defer *_defer // Много разных полей... } type _defer struct { heap bool sp uintptr pc uintptr fn func() link *_defer rangefunc bool head *atomic.Pointer[_defer] }
heap bool
- флаг, указывающий, где хранится defer, в куче или на стеке. Данный флаг необходим, если стек горутины будет переносится(да так иногда бывает), тогда Go нужно перенести defer которые хранятся в стеке. При этом, если defer хранится в куче, с ним по факту делать ничего не нужно.
До версии 1.14
все defer создавались в куче. Это просто и понятно. Начиная с 1.4
добавили оптимизацию Open-coded defer
и теперь defer может быть так же на стеке.
sp uintptr
- это значение(адрес в памяти) регистра Stack Pointer (указателя стека) в конкретный момент времени. Это значение необходимо, если стек "переезжает"(стек вырос и нужно скопировать данные в новый участок памяти ) или стек "схлопнулся" (при выходе из функции)
pc uintptr
- это значение(адрес в памяти) регистра Program Counter, которое необходимо для возврата, после выполнения отложенной функции.
fn func()
- это сам defer-функция, которую необходимо вызвать.
link *_defer
- в функции может быть объявлено много defer'ов. Они хранятся в виде связанного списка. Значение данного поле хранит ссылку на следующий defer.
rangefunc bool
— флаг, который указывает на оптимизацию выполнения defer
внутри цикла for range
по функции-генератору (например, по каналу или функции, возвращающей итератор). Начиная с Go 1.20, компилятор группирует все отложенные вызовы defer
, возникшие в ходе одной итерации, и помещает их в отдельный атомарный список head *atomic.Pointer[_defer]
. Это позволяет значительно снизить накладные расходы на выделение памяти и повысить производительность.
head *atomic.Pointer[_defer]
- если флаг rangefunc=true
, в этом поле хранится ссылка начало связанного списка defer'ов.
rangefunc bool
- флаг для оптимизации defer внутри циклов. Начиная с Go 1.20, компилятор может по-особому обрабатывать defer
внутри цикла, если цикл использует for range
по функции-генератору (например, по каналу или функции, возвращающей итератор).
Что-то вроде резюме
- defer хорошо использовать для отложенных функций который должны освободить ресурсы(закрыть канал/файл и т.д.), что-то залогировать и т.д.
- Не стоит использовать defer для громоздких задач (вроде запрос к внешнему API)
- defer'ы вызываются по принципу LIFO - последний пришёл первый ушёл
- defer'ы вызываются даже если произошла паника
- Вычисление параметров для defer'а происходят в момент объявления
- В каждой горутине свой стек defer'ов
🔥 Заходи в Телегу, там есть что почитать!