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

Давайте посмотрим, как это работает:

Отсрочка вызова функции, той же Close, имеет следующие плюсы:
- Гарантируется, что вы не забудете закрыть файл (такую ошибку легко допустить, если вы позже выполните редактирование функции для добавления нового пути возврата).
- Отсрочка вызова означает, что закрытие будет находиться рядом с открытием, а это гораздо понятнее, чем помещение в конец функции.
Аргументы для отложенной функции (включают получателя, когда функция является методом) оцениваются, если выполняется defer, а не в том случае, когда выполняется вызов (call). Такое положение вещей поможет избежать лишних забот о переменных, которые меняют значения в случае выполнения функции, что тоже значит, что отложить исполнение нескольких функций может лишь одно место отложенного вызова. Рассмотрим простой пример:

Важно отметить, что отложенные функции выполняются в соответствии с LIFO (last-in-first-out), следовательно, данный код вызовет «4 3 2 1 0» для печати после возврата из функции. Также можно написать пару простых трассировок:

Еще момент: аргументы отложенных функций оцениваются во время выполнения defer. Например, процедура трассировки способна установить аргумент для процедуры отслеживания. Смотрим пример ниже:

Будет напечатано следующее:

Для разработчиков, которые привыкли управлять ресурсами на уровне блоков на других языках программирования, оператор defer может показаться странным, однако его преимущество заключается в том, что он не на основе блоков, а на основе функций.
defer golang что это
defer в Golang — это ключевое слово, которое используется для отложенного выполнения функции или метода до тех пор, пока текущая функция не завершится. Когда встречается оператор defer , Golang добавляет вызов функции или метода в стек отложенных вызовов, а затем продолжает выполнение текущей функции.
defer может быть использовано для управления ресурсами, таких как файлы или сетевые соединения, чтобы гарантировать, что они будут закрыты независимо от того, как завершится функция. Он также может быть использован для обработки ошибок, когда функция должна быть завершена раньше времени.
Вот пример, который показывает использование defer для закрытия файла:
package main import ( "fmt" "os" ) func main() file, err := os.Open("example.txt") if err != nil panic(err) > // Закрыть файл после завершения функции defer file.Close() // . Код для чтения файла . >
В этом примере мы открываем файл example.txt и затем используем defer для закрытия файла после того, как функция завершится. Если произойдет ошибка при открытии файла, программа вызовет функцию panic , которая приведет к немедленному завершению программы.
Golang
Ключевое слово defer в Go позволяет нам запланировать запуск функций до возврата родительской функции. Несколько функций могут быть отложены (deferred) в родительской функции. defer часто используется для очистки ресурсов, завершения задач в области функций и т. п. Отложенные функции отлично подходят для удобства обслуживания. Откладывая, например, мы уменьшаем риск забыть закрыть файл в остальной части программы:
func main() < f, err := os.Open("hello.txt") if err != nil < log.Fatal(err) >defer f.Close() // Остальная часть программы. >
Отсрочка помогает нам, задерживая выполнение метода Close, позволяя нам печатать его, когда мы имеем правильный контекст. Именно так отложенные функции также помогают удобочитаемости исходного кода.
Как работает defer
Defer обрабатывает несколько функций, складывая их, и, следовательно, запускает их в порядке LIFO. Чем больше у вас отложенных функций, тем больше будет стек.
func main() < for i := 0; i < 5; i++ < defer fmt.Printf("%v ", i) >>
Приведенная выше программа выведет “4 3 2 1 0 ”, потому что последняя отложенная функция будет выполняться первой.
Когда функция откладывается, переменные, к которым она обращается, сохраняются в качестве аргументов. Для каждой отложенной функции компилятор генерирует вызов runtime.deferproc на месте вызова и вызывает в runtime.deferreturn точку возврата функции.
0: func run()
Компилятор сгенерирует код, подобный приведенному ниже, для программы выше:
runtime.deferproc(foo) // генерируется для строки 1 runtime.deferproc(bar) // генерируется для строки 2 // Другой код. runtime.deferreturn() // генерируется для строки 5
Производительность defer
defer раньше требовала двух дорогих вызовов времени выполнения, описанных выше. Это делало отложенные функции значительно более дорогими, чем неотложенные функции. Например, рассмотрим блокировку и разблокировку sync.Mutex с отложенным и не отложенным использованием.
var mu sync.Mutex mu.Lock() defer mu.Unlock()
Программа выше будет работать в 1,7 раза медленнее, чем не отложенная версия. Несмотря на то, что для блокировки и разблокировки мьютекса с помощью отсрочки требуется всего ~25-30 наносекунд, это имеет значение при широкомасштабном использовании или в случаях, когда вызов функции должен быть завершен менее чем за 10 наносекунд.
BenchmarkMutexNotDeferred-8 125341258 9.55 ns/op 0 B/op 0 allocs/op BenchmarkMutexDeferred-8 45980846 26.6 ns/op 0 B/op 0 allocs/op
Это накладные расходы, поэтому разработчики Go начали избегать отсрочек в некоторых случаях для повышения производительности. К сожалению, в этой ситуации разработчики Go ставят под угрозу читабельность.
Встраивание отложенных функций
В последних нескольких версиях Go были постепенные улучшения производительности defer. Но с Go 1.14 в некоторых распространенных случаях значительно улучшится производительность. Компилятор сгенерирует код для встраивания некоторых отложенных функций в точках возврата. С этим улучшением вызов некоторых отложенных функций будет стоить столько же, сколько и обычный вызов функции.
0: func run()
С новыми улучшениями приведенный выше код сгенерирует:
// Другой код. bar() // генерируется для строки 5 foo() // генерируется для строки 5
Это улучшение можно сделать только в статических случаях. Например, в цикле, где выполнение динамически определяется размером ввода, компилятор не имеет возможности сгенерировать код для встраивания всех отложенных функций. Но в простых случаях (например, откладывание наверху функции или в условных блоках, если они не находятся в циклах), можно встроить отложенные функции. Начиная с версии 1.14, простые случаи будут встроены, а координация во время выполнения потребуется только в том случае, если компилятор не может сгенерировать код.
Вот пример выполнения на Go 1.14beta с примером блокировки/разблокировки мьютекса выше. Отложенные и неотложенные версии теперь работают очень похоже:
BenchmarkMutexNotDeferred-8 123710856 9.64 ns/op 0 B/op 0 allocs/op BenchmarkMutexDeferred-8 104815354 11.5 ns/op 0 B/op 0 allocs/op
Go 1.14 — это хорошая версия для переоценки отсрочки, если вы избегаете defer для увеличения производительности.
- Основы Go: оператор defer
- Эффективный Go: оператор defer
- Defer, Panic, и Recover в Golang
Обработка ошибок в Go: Defer, Panic и Recover
В языке Go используются обычные способы управления потоком выполнения: if, for, switch, goto. Есть ещё оператор go, чтобы запустить код в отдельной го-процедуре. А сейчас я бы хотел обсудить менее обычные способы: defer, panic и recover.
Команда defer помещает вызов функции в список. Этот список отложенных вызовов выполняется после того, как объемлющая функция завершит выполнение. Defer обычно используется для упрощения функций, которые занимаются освобождением ресурса.
Например, посмотрим на функцию, которая открывает два файла и копирует содержимое из одного файла в другой:
func CopyFile(dstName, srcName string ) (written int64 , err os.Error ) src, err := os.Open(srcName, os.O_RDONLY, 0 )
if err != nil return
>
dst, err := os.Open(dstName, os.O_WRONLY|os.O_CREATE, 0644 )
if err != nil return
>
written, err = io.Copy(dst, src)
dst.Close()
src.Close()
return
>
Код рабочий, но в нём ошибка. Если второй вызов os.Open не удастся, функция завершит выполнение, оставив первый файл открытым. Это легко исправить, добавив вызов src.Close() перед вторым return’ом, но если функция посложнее, то эту проблему можно упустить. Введя команды defer, можно сделать, чтобы файлы закрылись при любых условиях:
func CopyFile(dstName, srcName string ) (written int64 , err os.Error ) src, err := os.Open(srcName, os.O_RDONLY, 0 )
if err != nil return
>
defer src.Close()
dst, err := os.Open(dstName, os.O_WRONLY|os.O_CREATE, 0644 )
if err != nil return
>
defer dst.Close()
return io.Copy(dst, src)
>
Команда defer позволяет подумать о закрытии файла сразу после его открытия, гарантируя, что файл будет закрыт, вне зависимости от количества точек выхода из функции.
Поведение команд defer простое и предсказуемое. Есть три лёгких правила:
1. Аргументы отложенного вызова функции вычисляются тогда, когда вычисляется команда defer.
В этом примере выражение «i» вычисляется, когда откладывается вызов Println. Отложенный вызов напечатает «0» после возврата из функции.
func a() i := 0
defer fmt.Println(i)
i++
return
>
2. Отложенные вызовы функций выполняются в порядке LIFO: последний отложенный вызов будет вызван первым — после того, как объемлющая функция завершит выполнение.
Эта функция напечатает «3210»:
func b() for i := 0 ; i < 4 ; i++ defer fmt.Print(i)
>
>
3. Отложенные функции могут читать и устанавливать именованные возвращаемые значения объемлющей функции.
В этом примере отложенная функция увеличивает возвращаемое значение i после того, как объемлющая функция завершит выполнение. Так, эта функция возвращает 2:
func c() (i int ) defer func () < i++ >()
return 1
>
Это удобный способ изменения кода ошибки, возвращаемого функцией. Скоро мы увидим пример этого.
Panic — это встроенная функция, которая останавливает обычный поток управления и начинает паниковать. Когда функция F вызывает panic, выполнение F останавливается, все отложенные вызовы в F выполняются нормально, затем F возвращает управление вызывающей функции. Для вызывающей функции вызов F ведёт себя как вызов panic. Процесс продолжается вверх по стеку, пока все функции в текущей го-процедуре не завершат выполнение, после чего аварийно останавливается программа. Паника может быть вызвана прямым вызовом panic, а также вследствие ошибок времени выполнения, таких как доступ вне границ массива.
Recover — это встроенная функция, которая восстанавливает контроль над паникующей го-процедурой. Recover полезна только внутри отложенного вызова функции. Во время нормального выполнения, recover возвращает nil и не имеет других эффектов. Если же текущая го-процедура паникует, то вызов recover возвращает значение, которое было передано panic и восстанавливает нормальное выполнение.
Вот пример программы, которая демонстрирует механику panic и defer:
package main
import «fmt»
func main() f()
fmt.Println( «Returned normally from f.» )
>
func f() defer func () if r := recover (); r != nil fmt.Println( «Recovered in f» , r)
>
>()
fmt.Println( «Calling g.» )
g( 0 )
fmt.Println( «Returned normally from g.» )
>
func g(i int ) if i > 3 fmt.Println( «Panicking!» )
panic (fmt.Sprintf( «%v» , i))
>
defer fmt.Println( «Defer in g» , i)
fmt.Println( «Printing in g» , i)
g(i+ 1 )
>
Функция g принимает на вход целое i и паникует, если i больше чем 3, или завёт себя с аргументом i+1. Функция f откладывает функцию, которая вызывает recover и печатает восстановленное значение (если оно не пустое). Попробуйте представить, что выведет эта программа, перед тем как читать далее.
Calling g. Printing in g 0 Printing in g 1 Printing in g 2 Printing in g 3 Panicking! Defer in g 3 Defer in g 2 Defer in g 1 Defer in g 0 Recovered in f 4 Returned normally from f.
Если мы уберем отложенный вызов функции из f, то паника не останавливается и достигает верха стека вызовов го-процедуры, останавливая программу. Так модифицированная программа выведет:
Calling g. Printing in g 0 Printing in g 1 Printing in g 2 Printing in g 3 Panicking! Defer in g 3 Defer in g 2 Defer in g 1 Defer in g 0 panic: 4 panic PC=0x2a9cd8 [Стек вызовов опущен]
Для реального примера использования panic и recover смотри пакет json из стандартной библиотеки Go. Он декодирует закодированные в JSON данные с помощью набора рекурсивных функций. Когда на вход поступает неправильно сформированный JSON, парсер вызывает panic, чтобы развернуть стек к верхнему вызову, который восстанавливается после паники и возвращает подходящий код ошибки (смотри функции «error» и «unmarshal» в decode.go). Похожий пример такой техники в процедуре Compile пакета regexp. Существует соглашение, что в библиотеках Go, даже если пакет использует panic внутри, его внешнее API возвращает явные коды ошибок.
Другие использования defer (помимо вышеупомянутого примера file.Close()) включают освобождение мутекса:
mu.Lock()
defer mu.Unlock()
печать нижнего колонтитула:
printHeader()
defer printFooter()
В итоге, команда defer (с или без panic и recover) предоставляет необычный и мощный механизм управления потоком выполнения. Она может быть использована для моделирования разных возможностей, за которые отвечают специальные структуры в других языках программирования. Попробуйте.
P.S. Замечания об опечатках и других неточностях просьба сообщать личными сообщениями, так как в силу разных причин смогу их исправлять только вечером.