Go defer: что не сказали в книгах

от автора

defer в Go — это мощный механизм для очистки ресурсов, закрытия файлов и разблокировки мьютексов. Вы наверняка слышали, что defer делает код чище и безопаснее.

Когда вы открываете файл через os.Open() или os.Create(), Go выделяет ресурс операционной системыдескриптор файла.

И вот в чём важный момент:

  • Этот дескриптор нужно обязательно закрыть через file.Close().

  • Иначе файл останется «висеть» открытым — ресурсы будут утекать, программа начнёт захлёбываться или упадёт.

Мьютекс (mutex = MUTual EXclusion) — это замок, который нужен, чтобы упорядочить доступ к общим данным из разных потоков (goroutines).

  • Только одна горутина может «захватить» мьютекс в один момент времени.

  • Остальные горутины будут ждать, пока замок не освободится.

Мьютекс — это способ сказать: «Сейчас только я работаю с этим куском данных, остальные — подождите!»

Но не всегда очевеидно, что если использовать defer бездумно, это может привести к серьёзным проблемам: снижению производительности, излишним аллокациям и неявным перегрузкам в runtime.

defer и инлайн: почему важно?

Go компилятор умеет делать инлайн функций — это значит, что вместо реального вызова код функции просто вставляется прямо в месте вызова. Это очень сильно ускоряет программу.

Но если в функции есть defer, инлайн невозможен. Почему? Потому что defer меняет логику выхода из функции, требуя дополнительную обработку.

Сколько стоит defer на практике?

Простой пример:

package main  import ( "fmt" "time" )  var x int  func withDefer() { defer func() {}() }  func withoutDefer() { x++ }  func main() { const iterations = 100_000_000  start := time.Now() for i := 0; i < iterations; i++ { withDefer() } fmt.Printf("withDefer: %v\n", time.Since(start))  start = time.Now() for i := 0; i < iterations; i++ { withoutDefer() } fmt.Printf("withoutDefer: %v\n", time.Since(start)) } 

Результаты:

go run main.go
withDefer: 89.974ms
withoutDefer: 132.4544ms

defer замедляет работу функции на порядок!

Как узнать, заинлайнил ли Go функцию?

Запустите:

go run -gcflags="-m" main.go

Вы увидите вывод типа:

./main.go:7:6: can inline add ./main.go:11:6: cannot inline withDefer: function contains defer ./main.go:16:10: inlining call to add

Здесь прямо написано: можно инлайнить или нельзя.

defer в runtime

Когда вы пишите defer, что происходит внутри?

  • Выделяется структура для deferred-вызовов.

  • В неё записываются функции и их аргументы.

  • При выходе из функции: выполняются все deferred-функции в обратном порядке.

Оптимизации

Начиная с Go 1.14 введены fast defer оптимизации, которые уменьшают накладные расходы на defer. Но в горячих циклах всё равно лучше избегать defer, если это возможно.

Вывод

  • defer чище код, но дешевым его не делает.

  • В горячих участках кода defer нужно использовать с осторожностью и только там, где это действительно оправдано.

  • Смотрите вывод -gcflags="-m", чтобы понять, заинлайнилась ли ваша функция

Горячий участок кода — это часть программы, которая выполняется очень часто или тратит много времени процессора.

  • Код внутри циклов, особенно больших (for, while),

  • Код, который вызывается миллионы раз (например, маленькие функции в обработке сетевых запросов),

  • Код, который напрямую влияет на быстродействие всей программы.


ссылка на оригинал статьи https://habr.com/ru/articles/904718/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *