Это отрывок из сообщения; полный пост доступен здесь: Golang Defer: From Basic To Trap.
Инструкция defer, вероятно, одна из первых вещей, которые мы находим довольно интересными, когда начинаем изучать Go, не так ли?
Но в нем есть еще много чего, что сбивает с толку многих людей, и есть много интересных аспектов, которые мы часто не затрагиваем при его использовании.
Например, оператор defer на самом деле имеет три типа (начиная с Go 1.22, хотя позже это может измениться): defer с открытым кодом, defer с выделением в куче и выделение стека. У каждого из них разная производительность и разные сценарии, в которых их лучше всего использовать. Это полезно знать, если вы хотите оптимизировать производительность.
В этом обсуждении мы рассмотрим все, от основ до более продвинутого использования, и даже немного, совсем немного, углубимся в некоторые внутренние детали.
Давайте кратко рассмотрим defer, прежде чем углубляться.
В Go defer — это ключевое слово, используемое для задержки выполнения функции до тех пор, пока не завершится окружающая функция.
func main() { defer fmt.Println("hello") fmt.Println("world") } // Output: // world // hello
В этом фрагменте оператор defer планирует выполнение fmt.Println("hello") в самом конце основной функции. Итак, fmt.Println("world") вызывается немедленно, и сначала печатается "world". После этого, поскольку мы использовали отсрочку, «привет» печатается как последний шаг перед основным завершением.
Это похоже на настройку задачи на последующий запуск, непосредственно перед завершением работы функции. Это действительно полезно для действий по очистке, таких как закрытие соединения с базой данных, освобождение мьютекса или закрытие файла:
func doSomething() error { f, err := os.Open("phuong-secrets.txt") if err != nil { return err } defer f.Close() // ... }
Приведенный выше код — хороший пример, показывающий, как работает defer, но это также плохой способ использования defer. Мы поговорим об этом в следующем разделе.
"Хорошо, хорошо, но почему бы не поставить f.Close() в конце?"
Для этого есть несколько веских причин:
Когда случается паника, стек разматывается и отложенные функции выполняются в определенном порядке, который мы рассмотрим в следующем разделе.
Когда вы используете в функции несколько операторов отсрочки, они выполняются в «стековом» порядке, то есть последняя отложенная функция выполняется первой.
func main() { defer fmt.Println(1) defer fmt.Println(2) defer fmt.Println(3) } // Output: // 3 // 2 // 1
Каждый раз, когда вы вызываете оператор defer, вы добавляете эту функцию в начало связанного списка текущей горутины, например:
И когда функция завершает работу, она проходит через связанный список и выполняет каждый из них в порядке, показанном на изображении выше.
Но помните, что он не выполняет все задержки в связанном списке горутины, он запускает только задержки в возвращаемой функции, потому что наш связанный список отсрочек может содержать множество отложений из множества разных функций.
func B() { defer fmt.Println(1) defer fmt.Println(2) A() } func A() { defer fmt.Println(3) defer fmt.Println(4) }
Итак, выполняются только отложенные функции в текущей функции (или текущем кадре стека).
Но есть один типичный случай, когда все отложенные функции в текущей горутине отслеживаются и выполняются, и тогда случается паника.
Помимо ошибок времени компиляции, у нас есть куча ошибок времени выполнения: деление на ноль (только целые числа), выход за пределы, разыменование нулевого указателя и т. д. Эти ошибки вызывают панику приложения.
Паника — это способ остановить выполнение текущей горутины, развернуть стек и выполнить отложенные функции в текущей горутине, что приведет к сбою нашего приложения.
Чтобы обработать непредвиденные ошибки и предотвратить сбой приложения, вы можете использовать функцию восстановления в отложенной функции, чтобы восстановить контроль над паникующей горутиной.
func main() { defer func() { if r := recover(); r != nil { fmt.Println("Recovered:", r) } }() panic("This is a panic") } // Output: // Recovered: This is a panic
Обычно люди в панике выдают ошибку и ловят ее с помощью восстановления(..), но это может быть что угодно: строка, целое число и т. д.
В приведенном выше примере единственное место, где можно использовать восстановление, — внутри отложенной функции. Позвольте мне объяснить это немного подробнее.
Здесь можно перечислить пару ошибок. Я видел как минимум три подобных фрагмента в реальном коде.
Первый вариант — использование восстановления непосредственно в качестве отложенной функции:
func main() { defer recover() panic("This is a panic") }
Приведенный выше код все еще вызывает панику, и это задумано средой выполнения Go.
Функция восстановления предназначена для обнаружения паники, но для правильной работы ее необходимо вызывать внутри отложенной функции.
За кулисами наш вызов восстановления на самом деле является runtime.gorecover, и он проверяет, что вызов восстановления происходит в правильном контексте, в частности, из правильной отложенной функции, которая была активна, когда произошла паника.
"Значит ли это, что мы не можем использовать восстановление в функции внутри отложенной функции, как здесь?"
func myRecover() { if r := recover(); r != nil { fmt.Println("Recovered:", r) } } func main() { defer func() { myRecover() // ... }() panic("This is a panic") }
Именно, приведенный выше код не будет работать так, как вы ожидаете. Это связано с тем, что восстановление вызывается не напрямую из отложенной функции, а из вложенной функции.
Теперь еще одна ошибка — попытка поймать панику из другой горутины:
func main() { defer func() { if r := recover(); r != nil { fmt.Println("Recovered:", r) } }() go panic("This is a panic") time.Sleep(1 * time.Second) // Wait for the goroutine to finish }
Разумно, правда? Мы уже знаем, что цепочки отсрочки принадлежат конкретной горутине. Было бы сложно, если бы одна горутина могла вмешиваться в работу другой, чтобы справиться с паникой, поскольку каждая горутина имеет свой собственный стек.
К сожалению, единственный выход в этом случае — сбой приложения, если мы не справимся с паникой в этой горутине.
Я уже сталкивался с этой проблемой, когда старые данные попадали в систему аналитики, и было сложно понять, почему.
Вот что я имею в виду:
func pushAnalytic(a int) { fmt.Println(a) } func main() { a := 10 defer pushAnalytic(a) a = 20 }
Как вы думаете, каким будет результат? Это 10, а не 20.
Это потому, что когда вы используете оператор defer, он сразу же захватывает значения. Это называется «захват по значению». Таким образом, значение a, которое отправляется в pushAnalytic, устанавливается равным 10, когда запланирована отсрочка, даже если оно изменится позже.
Есть два способа исправить это.
...
Полную публикацию можно найти здесь: Golang Defer: From Basic To Trap.
Отказ от ответственности: Все предоставленные ресурсы частично взяты из Интернета. В случае нарушения ваших авторских прав или других прав и интересов, пожалуйста, объясните подробные причины и предоставьте доказательства авторских прав или прав и интересов, а затем отправьте их по электронной почте: [email protected]. Мы сделаем это за вас как можно скорее.
Copyright© 2022 湘ICP备2022001581号-3