Что такое 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.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Daniel Milde: Recover in Golang

Предыдущая статьяМы забываем основы фронтенд-разработки
Следующая статья10 полезных методик во фронтенд-разработке React