Как стать автором
Обновить

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

Уровень сложностиПростой
Время на прочтение3 мин
Количество просмотров4K

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 замедляет работу функции на порядок!

Пример от @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 функцию?

Запустите:

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),

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

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

Теги:
Хабы:
+4
Комментарии18

Публикации

Работа

PHP программист
79 вакансий
Go разработчик
81 вакансия

Ближайшие события