Не без паники в Go
В то время, как обсуждается возможный новый дизайн обработки ошибок и ведутся споры о преимуществах явной обработки ошибок, предлагаю рассмотреть некоторые особенности ошибок, паник и их восстановления в Go, которые будут полезны на практике.
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
Объявление нескольких типов ошибок, каждая имеет свои данные:
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() }
Функция которая может вернуть эти ошибки:
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 }
Обработка ошибок через приведения типов:
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 будет записано такая информация:
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) }
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 }
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") }
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
На сегодня все. Спасибо!
Комментарии
Отправить комментарий