Recover — очень интересный, мощный функционал Golang. Мы в Outreach.io применяем его для обработки ошибок в Kubernetes.
Panic/defer/recover — это, по сути, альтернативы Golang концепциям throw/finally/catch других языков программирования. Имея общую основу, в важных деталях они различаются.
Defer
Для полного понимания recover рассмотрим сначала операторы defer. Когда ключевое слово defer добавляется к вызову функции, этот вызов выполняется непосредственно перед возвратом текущей функции.
Если в функции несколько операторов defer, они выполняются, начиная с последнего и заканчивая первым, чем очень упрощается создание логики очистки, как показано в примере ниже:
package main
import (
"context"
"database/sql"
"fmt"
)
func readRecords(ctx context.Context) error {
db, err := sql.Open("sqlite3", "file:test.db?cache=shared&mode=memory")
if err != nil {
return err
}
defer db.Close() // этот вызов функции выполнится третьим, когда функция «readRecords» вернется
conn, err := db.Conn(ctx)
if err != nil {
return err
}
defer conn.Close() // этот вызов функции выполнится вторым
rows, err := conn.QueryContext(ctx, "SELECT id FROM users")
if err != nil {
return err
}
defer rows.Close() // этот вызов функции выполнится первым
for rows.Next() {
var id int64
if err := rows.Scan(&id); err != nil {
return err
}
fmt.Println("ID:", id)
}
return nil
}
func main() {
readRecords(context.Background())
}
Panic
Теперь обратимся к функции panic, ею текущая горутина переключается в режим паники.
Обычный поток выполнения в текущей функции останавливается, выполняются лишь операторы defer. Затем то же происходит с вызывающей функцией, таким образом стек переходит к верхней функции main
, после чего программа аварийно завершается.
Panic вызывается напрямую передачей одного значения как аргумента либо к ней приводят ошибки времени выполнения, например, разыменованием пустого указателя:
package main
import "fmt"
func main() {
var x *string
fmt.Println(*x)
}
// panic: ошибка времени выполнения: некорректный адрес памяти или разыменование пустого указателя
Recover
Recover — встроенная функция для восстановления контроля в случае паники. Активируется она, только если вызывается внутри функции отложенного вызова, а если вне ее — всегда возвращается только nil.
В режиме паники, когда вызывается функция recover, возвращается переданное функции panic значение. Простой пример:
package main
import "fmt"
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Recovered: %v\n", r)
}
}()
panic("spam, egg, sausage, and spam")
}
// Восстановлено: «spam», «egg», «sausage» и «spam»
Так же восстанавливаются и после ошибок времени выполнения:
package main
import "fmt"
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Recovered: %v\n", r)
}
}()
var x *string
fmt.Println(*x)
}
// Восстановлено: ошибка времени выполнения: некорректный адрес памяти или разыменование пустого указателя
В данном случае тип возвращаемого recover значения — error, точнее, runtime.errorString.
Имеется одно ограничение: значения не возвращаются непосредственно из блока recover, потому что оператор return внутри него возвращается только из функции отложенного вызова, а не из самой внешней функции:
package main
import "fmt"
func foo() int {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Recovered: %v\n", r)
return 1 // «слишком много возвращаемых значений», потому что мы возвращаем только из анонимной функции
}
}()
panic("spam, egg, sausage, and spam")
}
func main() {
x := foo()
fmt.Println(x)
}
Чтобы поменять возвращаемое функцией значение, воспользуемся именованными возвращаемыми значениями:
package main
import "fmt"
func foo() (ret int) {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Recovered: %v\n", r)
ret = 1
}
}()
panic("spam, egg, sausage, and spam")
}
func main() {
x := foo()
fmt.Println("value:", x)
}
// Восстановлено: «spam», «egg», «sausage» и «spam»
// значение: 1
Вот реальный пример преобразования паники в обычную ошибку:
package main
import (
"fmt"
"github.com/google/uuid"
)
// попытка преобразования «processInput» строки ввода в «uuid.UUID»
// так паника преобразуется в ошибку
func processInput(input string) (u uuid.UUID, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v", r)
}
}()
// какая-то логика, в том числе сторонняя, чреватая паникой, например такая:
u = uuid.MustParse(input)
return u, nil
}
func main() {
u, err := processInput("xxx")
if err != nil {
fmt.Println(err)
}
fmt.Println(u)
}
// panic: uuid: Parse(xxx): недопустимая длина UUID: 3
// 00000000-0000-0000-0000-000000000000
Попробуем что-то сложнее. Напишем для Kubernetes универсальную функцию recover, которой обрабатываются все неперехваченные паники и ошибки времени выполнения, а также собираются трассировки стека для них, чтобы структурированно их регистрировать в формате json:
package main
import (
"fmt"
"log/slog"
"os"
"github.com/pkg/errors"
)
func foo() string {
var s *string
return *s
}
func handlePanic(r interface{}) error {
var errWithStack error
if err, ok := r.(error); ok {
errWithStack = errors.WithStack(err)
} else {
errWithStack = errors.Errorf("%+v", r)
}
return errWithStack
}
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
defer func() {
if r := recover(); r != nil {
err := handlePanic(r)
logger.Error(
"panic occurred",
"msg", err.Error(),
"stack", fmt.Sprintf("%+v", err),
)
}
}()
fmt.Println(foo())
}
// {
// "time":"2009-11-10T23:00:00Z",
// "level":"ERROR",
// "msg":"panic occurred",
// "msg": «ошибка времени выполнения: некорректный адрес памяти или разыменование пустого указателя»,
// "stack": «ошибка времени выполнения: некорректный адрес памяти или разыменование пустого указателя\nmain.handlePanic\n\t/tmp/sandbox239055659/prog.go:19\nmain.main.func1...»
// }
На сегодня это все. Функция recover, хотя применяется разработчиками Golang нечасто, в некоторых ситуациях очень полезна.
Важно
Будучи сопоставимы с throw/except, panic/recover используются в других ситуациях: никогда для обычных потоков, например для ошибок пользователя вроде ошибок валидации и т. д. Поэтому для ожидаемой ошибки используйте стандартное возвращаемое значение error.
Читайте также:
- Мифы Go, в которые мы верим: емкость
- 10 проектов для изучения Golang в 2023 году
- Как построить масштабируемый API на Go с помощью Gin
Читайте нас в Telegram, VK и Дзен
Перевод статьи Daniel Milde: Recover in Golang