Лучший опыт

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