Что такое Recover в Golang?.
Recover — очень интересный, мощный функционал Golang. Мы в Outreach.io применяем его для обработки ошибок в Kubernetes.
Panic/defer/recover — это, по сути, альтернативы Golang концепциям throw/finally/catch других языков программирования. Имея общую основу, в важных деталях они различаются. Defer
Для полного понимания recover рассмотрим сначала операторы defer. Когда ключевое слово defer добавляется к вызову функции, этот вызов выполняется непосредственно перед возвратом т
Что такое Recover в Golang?...
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.