Не без паники в Go

от автора

Привет, уважаемые читатели Хабрахабра. В то время, как обсуждается возможный новый дизайн обработки ошибок и ведутся споры о преимуществах явной обработки ошибок, предлагаю рассмотреть некоторые особенности ошибок, паник и их восстановления в Go, которые будут полезны на практике.
image

error

error это интерфейс. И как большинство интерфейсов в Go, определение error краткое и простое:

type error interface {     Error() string }

Получается любой тип у которого есть метод Error может быть использован как ошибка. Как учил Роб Пайк Ошибки это значения, а значениями можно оперировать и программировать различную логику.

В стандартной библиотеки Go имеются две функции, которые удобно использовать для создания ошибок. Функция errors.New хорошо подходит для создания простых ошибок. Функция fmt.Errorf позволяет использовать стандартное форматирования.

err := errors.New("emit macho dwarf: elf header corrupted")  const name, id = "bimmler", 17 err := fmt.Errorf("user %q (id %d) not found", name, id)

Обычно для работы с ошибками достаточно типа error. Но иногда может потребоваться передавать с ошибкой дополнительную информацию, в таких случаях можно добавить свой тип ошибок.
Неплохой пример это тип PathError из пакета os

// PathError records an error and the operation and file path that caused it. type PathError struct {     Op   string     Path string     Err  error }  func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }

Значение такой ошибки будет содержать операцию, путь и ошибку.
Инициализируются они таким образом:

... return nil, &PathError{"open", name, syscall.ENOENT} ... return nil, &PathError{"close", file.name, e}

Обработка может иметь стандартный вид:

_, err := os.Open("---") if err != nil{     fmt.Println(err) } // open ---: The system cannot find the file specified.

А вот если есть необходимость получить дополнительную информацию, то можно распаковать error в *os.PathError:

_, err := os.Open("---") if pe, ok := err.(*os.PathError);ok{     fmt.Printf("Err: %s\n", pe.Err)     fmt.Printf("Op: %s\n", pe.Op)     fmt.Printf("Path: %s\n", pe.Path) } // Err: The system cannot find the file specified. // Op: open // Path: ---

Этот же подход можно применять если функция может вернуть несколько различных типов ошибок.
play
Объявление нескольких типов ошибок, каждая имеет свои данные:

code

type ErrTimeout struct {     Time time.Duration     Err  error } func (e *ErrTimeout) Error() string { return e.Time.String() + ": " + e.Err.Error() }  type ErrPermission struct {     Status string     Err  error } func (e *ErrPermission) Error() string { return e.Status + ": " + e.Err.Error() }

Функция которая может вернуть эти ошибки:

code

func proc(n int) error {     if n <= 10 {         return &ErrTimeout{Time: time.Second * 10, Err: errors.New("timeout error")}     } else if n >= 10 {         return &ErrPermission{Status: "access_denied", Err: errors.New("permission denied")}     }     return nil }

Обработка ошибок через приведения типов:

code

func main(){     err := proc(11)     if err != nil {         switch e := err.(type) {         case *ErrTimeout:             fmt.Printf("Timeout: %s\n", e.Time.String())             fmt.Printf("Error: %s\n", e.Err)         case *ErrPermission:             fmt.Printf("Status: %s\n", e.Status)             fmt.Printf("Error: %s\n", e.Err)         default:             fmt.Println("hm?")             os.Exit(1)         }     } }

В случае когда ошибкам не нужны специальные свойства, в Go хорошей практикой считается создавать переменные для хранения ошибок на уровне пакетов. Примером может служить такие ошибки как io.EOF, io.ErrNoProgress и пр
В примере ниже, прерываем чтение и продолжаем работу приложения, когда ошибка равна io.EOF или закрываем приложения при любых других ошибках.

func main(){     reader := strings.NewReader("hello world")     p := make([]byte, 2)      for {         _, err := reader.Read(p)         if err != nil{             if err == io.EOF {                 break             }             log.Fatal(err)         }     } }

Это эффективно, поскольку ошибки создаются только один раз и используются многократно.

stack trace

Список функций, вызванных в момент захвата стека. Трассировка стека помогает получить более полное представление о происходящем в системе. Сохранение трассировки в логах может серьезно помочь при отладки.
Наличие этой информации в ошибке у Go часто не хватает, но к счастью получить дампа стека в Go не сложно.
Для вывода трассировки в стандартный выводов можно воспользоваться debug.PrintStack():

func main(){     foo() }  func foo(){     bar() } func bar(){     debug.PrintStack() }

Как результат в Stderr будет записано такая информация:

stack

goroutine 1 [running]: runtime/debug.Stack(0x1, 0x7, 0xc04207ff78)         .../Go/src/runtime/debug/stack.go:24 +0xae runtime/debug.PrintStack()         .../Go/src/runtime/debug/stack.go:16 +0x29 main.bar()         .../main.go:13 +0x27 main.foo()         .../main.go:10 +0x27 main.main()         .../main.go:6 +0x27 

debug.Stack() возвращает слайс байт с дампом стека, который можно в дальнейшем вывести в журнал или в другом месте.

b := debug.Stack() fmt.Printf("Trace:\n %s\n", b)

Есть еще один момент, если мы сделаем вот так:

go bar()

то на выходе получим такую информацию:

main.bar()         .../main.go:19 +0x2d created by main.foo         .../main.go:14 +0x3c

У каждой горутины отдельный стек, соответственно, мы получаем только его дамп. Кстати, о своих стеках у горутин, с этим еще связана работа recover, но об этом чуть позже.
И так, что бы увидеть информацию по всем горутинам, можно воспользоваться runtime.Stack() и передать вторым аргументом true.

func bar(){     buf := make([]byte, 1024)     for {         n := runtime.Stack(buf, true)         if n < len(buf) {             break         }         buf = make([]byte, 2*len(buf))     }     fmt.Printf("Trace:\n %s\n", buf) }

stack

Trace:  goroutine 5 [running]: main.bar()         .../main.go:21 +0xbc created by main.foo         .../main.go:14 +0x3c  goroutine 1 [sleep]: time.Sleep(0x77359400)         .../Go/src/runtime/time.go:102 +0x17b main.foo()         .../main.go:16 +0x49 main.main()         .../main.go:10 +0x27 

Добавим в ошибку эту информацию и тем самым сильно повысим ее информативность.
Например так:

type ErrStack struct {     StackTrace []byte     Err  error } func (e *ErrStack) Error() string {     var buf bytes.Buffer     fmt.Fprintf(&buf, "Error:\n %s\n", e.Err)     fmt.Fprintf(&buf, "Trace:\n %s\n", e.StackTrace)     return buf.String() }

Можно добавить функцию для создания этой ошибки:

func NewErrStack(msg string) *ErrStack {     buf := make([]byte, 1024)     for {         n := runtime.Stack(buf, true)         if n < len(buf) {             break         }         buf = make([]byte, 2*len(buf))     }     return &ErrStack{StackTrace: buf, Err: errors.New(msg)} }

Дальше с этим уже можно работать:

func main() {     err := foo()     if err != nil {         fmt.Println(err)     } }  func foo() error{     return bar() } func bar() error{     err := NewErrStack("error")     return err }

stack

Error:  error Trace:  goroutine 1 [running]: main.NewErrStack(0x4c021f, 0x5, 0x4a92e0)         .../main.go:41 +0xae main.bar(0xc04207ff38, 0xc04207ff78)         .../main.go:24 +0x3d main.foo(0x0, 0x48ebff)         .../main.go:21 +0x29 main.main()         .../main.go:11 +0x29 

Соответственно ошибку и трейс можно рзаделить:

func main(){     err := foo()      if st, ok := err.(*ErrStack);ok{         fmt.Printf("Error:\n %s\n", st.Err)         fmt.Printf("Trace:\n %s\n", st.StackTrace)     } }

И конечно уже есть готовые решение. Одно из них, это пакет https://github.com/pkg/errors. Он позволяет создавать новую ошибку, которая уже будет содержать стек трейс, а можно добавлять трейс и/или дополнительное сообщения к уже существующей ошибке. Плюс удобное форматирование вывода.

import (     "fmt"     "github.com/pkg/errors" )  func main(){     err := foo()     if err != nil {         fmt.Printf("%+v", err)     } }  func foo() error{     err := bar()     return errors.Wrap(err, "error2") } func bar() error{     return errors.New("error") }

stack

error main.bar         .../main.go:20 main.foo         .../main.go:16 main.main         .../main.go:9 runtime.main         .../Go/src/runtime/proc.go:198 runtime.goexit         .../Go/src/runtime/asm_amd64.s:2361 error2 main.foo         .../main.go:17 main.main         .../main.go:9 runtime.main         .../Go/src/runtime/proc.go:198 runtime.goexit         .../Go/src/runtime/asm_amd64.s:2361

%v выведет только сообщения

error2: error

panic/recover

Паника(aka авария, aka panic), как правило, сигнализирует о наличии неполадок, из-за которых система (или конкретная подсистема) не может продолжать функционировать. В случае вызова panic среда выполнения Go просматривает стек, пытаясь найти для нее обработчик. Необработанные паники прекращают работу приложения. Это принципиально отличает их от ошибок, которые позволяют не обрабатывать себя.

В вызов функции panic можно передать любой аргумент.

panic(v interface{})

Удобно в panic передать ошибку, того типа который упростит восстановления и поможет отладки.

panic(errors.New("error"))

Восстановление после аварии в Go основывается на отложенном вызове функций, он же defer. Такая функция гарантировано будет выполнена в момент возврата из родительской функции. Не зависимо от причины — оператор return, конец функции или паника.
А вот уже функция recover дает возможность получить информацию об аварии и остановить раскручивание стека вызовов.
Типичный пример вызова panic и обработчик:

func main(){     defer func() {         if err := recover(); err != nil{             fmt.Printf("panic: %s", err)         }     }()     foo() }  func foo(){     panic(errors.New("error")) }

recover возвращает interface{} (тот самый который передаем в panic) или nil, если не было вызова panic.

Рассмотрим еще один пример обработки аварийных ситуаций. У нас есть некоторая функция в которую мы передаем например ресурс и которая в теории может вызвать панику.

func bar(f *os.File) {     panic(errors.New("error")) }

Во-первых, может понадобится всегда выполнять какие то действия при завершении, например очистка ресурсов, в нашем случае это закрытия файла.
Во-вторых, некорректное выполнение такой функции не должно приводить к завершению всей программы.
Такую задачу можно решить с помощью defer, recover и замыкания:

func foo()(err error) {     file, _ := os.Open("file")     defer func() {         if r := recover(); r != nil {             err = r.(error) // обрабатываем аварийную ситуацию, распаковываем если знаем, что в панике ошибка             // err := errors.New("trapped panic: %s (%T)", r, r) // или создаем свою ошибку         }         file.Close() // закрываем файл     }()      bar(file)      return err }

Замыкание позволяем обратится к выше объявленным переменным, благодаря этому гарантировано закрываем файл и в случае аварии, извлечь из нее ошибку и передать ее обычному механизму обработки ошибок.

Бывают обратные ситуации, когда функция c определенными аргументами всегда должна отрабатывать корректно и если этого не происходит, то что пошло совсем плохо.
В подобных случаях добавляют функцию обертку в которой вызывается целевая функция и в случае ошибки вызывается panic.
В Go обычно такие функции с префиксом Must:

// MustCompile is like Compile but panics if the expression cannot be parsed. // It simplifies safe initialization of global variables holding compiled regular // expressions. func MustCompile(str string) *Regexp {     regexp, error := Compile(str)     if error != nil {         panic(`regexp: Compile(` + quote(str) + `): ` + error.Error())     }     return regexp }

// Must is a helper that wraps a call to a function returning (*Template, error) // and panics if the error is non-nil. It is intended for use in variable initializations // such as //  var t = template.Must(template.New("name").Parse("html")) func Must(t *Template, err error) *Template {     if err != nil {         panic(err)     }     return t }

Стоит помнить еще про один момент, связанный с panic и горутинами.
Часть тезисов из того что обсудили выше:

  • Для каждой горутины выделяется отдельный стек.
  • При вызове panic, в стеке ищется recover.
  • В случае, когда recover не найдет, завершается все приложение.

Обработчик в main не перехватит панику из foo и программа аварийно завершится:

func main(){     defer func() {         if err := recover(); err != nil{             fmt.Printf("panic: %s", err)         }     }()      go foo()      time.Sleep(time.Minute) } func foo(){     panic(errors.New("error")) }

Это будет проблемой, если например вызываются обработчик для соединения на сервере. В случае паники в любом из обработчиков, весь сервер завершит выполнение. А контролировать обработку аварий в этих функциях, по какой то причине, вы не можете.
В простом случае решение может выглядит примерно так:

type f func()  func Def(fn f) {     go func() {         defer func() {             if err := recover(); err != nil {                 log.Println("panic")             }         }()          fn()     }() }  func main() {     Def(foo)      time.Sleep(time.Minute) }  func foo() {     panic(errors.New("error")) }

handle/check

Возможно в будущем нас ждут изменения в обработки ошибок. Ознакомится с ними можно по ссылкам:
go2draft
Обработка ошибок в Go 2

На сегодня все. Спасибо!


ссылка на оригинал статьи https://habr.com/post/422419/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *