Go defer: что не сказали в книгах
- понедельник, 28 апреля 2025 г. в 00:00:10
defer
в Go — это мощный механизм для очистки ресурсов, закрытия файлов и разблокировки мьютексов. Вы наверняка слышали, что defer
делает код чище и безопаснее.
Когда вы открываете файл через os.Open
()
или os.Create()
, Go выделяет ресурс операционной системы — дескриптор файла.
И вот в чём важный момент:
Этот дескриптор нужно обязательно закрыть через file.Close()
.
Иначе файл останется "висеть" открытым — ресурсы будут утекать, программа начнёт захлёбываться или упадёт.
Мьютекс (mutex = MUTual EXclusion) — это замок, который нужен, чтобы упорядочить доступ к общим данным из разных потоков (goroutines).
Только одна горутина может "захватить" мьютекс в один момент времени.
Остальные горутины будут ждать, пока замок не освободится.
Мьютекс — это способ сказать: «Сейчас только я работаю с этим куском данных, остальные — подождите!»
Но не всегда очевеидно, что если использовать defer
бездумно, это может привести к серьёзным проблемам: снижению производительности, излишним аллокациям и неявным перегрузкам в runtime.
Go компилятор умеет делать инлайн функций — это значит, что вместо реального вызова код функции просто вставляется прямо в месте вызова. Это очень сильно ускоряет программу.
Но если в функции есть 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
замедляет работу функции на порядок!
Пример от @koreychenko
package main
import (
"fmt"
"os"
"time"
)
func withDefer() {
file, err := os.Open("test.txt")
if err != nil {
panic(err)
}
defer file.Close()
}
func withoutDefer() {
file, err := os.Open("test.txt")
if err != nil {
panic(err)
}
file.Close()
}
func main() {
const iterations = 100_000_0
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))
}
Результаты:
withDefer: 6.810516492s
withoutDefer: 6.829267096s
Запустите:
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
, что происходит внутри?
Выделяется структура для deferred-вызовов.
В неё записываются функции и их аргументы.
При выходе из функции: выполняются все deferred-функции в обратном порядке.
Начиная с Go 1.14 введены fast defer оптимизации, которые уменьшают накладные расходы на defer
. Но в горячих циклах всё равно лучше избегать defer
, если это возможно.
defer
чище код, но дешевым его не делает.
В горячих участках кода defer
нужно использовать с осторожностью и только там, где это действительно оправдано.
Смотрите вывод -gcflags="-m"
, чтобы понять, заинлайнилась ли ваша функция
Горячий участок кода — это часть программы, которая выполняется очень часто или тратит много времени процессора.
Код внутри циклов, особенно больших (
for
,while
),Код, который вызывается миллионы раз (например, маленькие функции в обработке сетевых запросов),
Код, который напрямую влияет на быстродействие всей программы.