golang
July 1

Про 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'ов

🔥 Заходи в Телегу, там есть что почитать!