вторник

Обработка ошибок в Go


Введение


Если вы писали какой-нибудь код на языке Go, то вероятно встречали встроенный тип error. В коде Go, error используется, чтобы указать на аварийный режим работы. Например, функция os.Open возвращает ненулевое значение error, когда не может открыть файл.
func Open(name string) (file *File, err error)
Следующий код для открытия файла, использует os.Open. Если произойдет ошибка, то будет вызван log.Fatal, чтобы напечатать сообщение об ошибке и остановиться.
f, err := os.Open("filename.ext")
if err != nil {
    log.Fatal(err)
}
// Что-нибудь делаете с *File f
Зная о типе error только это, вы сможете многое сделать в Go, но в этой статья мы более внимательно рассмотрим error и обсудим, некоторые хорошие вещи, которые даются нам для обработки ошибок язык Go.



Тип error


Тип error - это интерфейс. Переменная error представляет любое значение, которое может описать себя как строка. Вот объявление интерфейса:
type error interface {
    Error() string
}
Тип error, как все встроенные типы предопределена в универсальном блоке.

Наиболее широко применяемая реализация error из пакета errors показана в примере не экспортируемого типа errorString.
// errorString - тривиальная реализация error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}
Можно создать одно из этих значений функцией errors.New. Она берет строку и преобразовывает в errors.errorString и возвращает как значение error.
// New returns an error that formats as the given text.
func New(text string) error {
    return &errorString{text}
}
Вот как вы могли бы использовать errors.New:
func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return 0, errors.New("math: square root of negative number")
    }
    // реализация
}
Вызывающая программа передает отрицательный параметр в Sqrt, и получает не пустое значение error (чьим конкретным представлением являет значение errors.errorString). Вызывающая программа может получить доступ к строке ошибки ("math: square root of...”) вызвав метод Error или просто вывести её:
f, err := Sqrt(-1)
if err != nil {
    fmt.Println(err)
}
Пакет fmt форматирует значение error, вызывая его метод Error() string.

Возвращенная ошибка os.Open, выводится как “open /etc/passwd: permission denied”, а не просто “permission denied”. В возвращаемой ошибке Sqrt отсутствует информация о неверном параметре.

Чтобы добавить такую информацию, можно воспользоваться функцией Errorf из пакета fmt. Она форматирует строку по правилам “Printf” и возвращает error, созданную errors.New.
if f < 0 {
    return 0, fmt.Errorf("math: square root of negative number %g", f)
}
Во многих случаях fmt.Errorf достаточно хороша, но так как error - это интерфейс, то вы можете использовать произвольную структуру данных как значение error, чтобы позволить вызывающей программе, изучить детали ошибки.

Например, наша гипотетическая вызывающая программа, могла бы захотеть получить назад неправильный параметр, переданный в Sqrt. Мы можем сделать это определив новую реализацию ошибки, используя errors.errorString:
type NegativeSqrtError float64

func (f NegativeSqrtError) Error() string {
    return fmt.Sprintf("math: square root of negative number %g", float64(f))
}
Изощренная вызывающая программа может использовать приведение типов, чтобы проверить NegativeSqrtError и особым образом её обработать. В то время как простая передача ошибки в fmt.Println или log.Fatal не будет видеть изменений в поведении.

Как другой пример, пакет json, определяющий тип SyntaxError, который возвращается функцией json.Decode, когда она встречается с синтаксической ошибкой при разборе “блоб” JSON.
type SyntaxError struct {
    msg    string // описание ошибки
    Offset int64  // произошедшая ошибка после чтения Offset bytes
}

func (e *SyntaxError) Error() string { return e.msg }
Поле Offset даже не показывается в форматировании ошибки по умолчанию, но вызывающая программа может использовать его для добавления в сообщение об ошибке информацию о файле и строке:
if err := dec.Decode(&val); err != nil {
    if serr, ok := err.(*json.SyntaxError); ok {
        line, col := findLine(f, serr.Offset)
        return fmt.Errorf("%s:%d:%d: %v", f.Name(), line, col, err)
    }
    return err
}
(Это немного упрощенная версия фактического кода из проекта Camlistore.)

Интерфейс error, требует только метод Error. У конкретной реализации ошибки, могут быть дополнительные методы. Например, пакет net, возвращает ошибки типа error, следуя обычному соглашению, но некоторые реализации ошибок, имеют дополнительные методы, определенные интерфейсом net.Error:
package net

type Error interface {
    error
    Timeout() bool   // Тайм-аут ошибки?
    Temporary() bool // Временная ошибка?
}
Клиентский код может использовать приведение типов для проверки net.Error, чтобы затем отличить случайные ошибки от постоянных. Например, поисковой робот мог бы перейти в режим ожидания и снова повторить попытку при встрече с временной ошибкой, а в другом случае уйти.
if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
    time.Sleep(1e9)
    continue
}
if err != nil {
    log.Fatal(err)
}

Упрощенная обработка повторяющихся ошибок 


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

Рассмотрим приложение App Engine с обработчиком HTTP, который получает запись из хранилища данных и форматирует её с шаблоном.
func init() {
    http.HandleFunc("/view", viewRecord)
}

func viewRecord(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    if err := viewTemplate.Execute(w, record); err != nil {
        http.Error(w, err.Error(), 500)
    }
}
Эта функция обрабатывает ошибки функции datastore.Get и метода viewTemplate.Execute. В обоих случает представляется простое сообщение об ошибки с кодом состояния HTTP 500 ("Internal Server Error"). Это походит на управляемый объем кода, но добавьте больше обработчиков HTTP и вы быстро закончите большим количеством одинаковых копий кода для обработки ошибки.

Чтобы сократить дублирование кода, вы можете определить свой собственный HTTP тип appHandler, который включает возврат значения error:
type appHandler func(http.ResponseWriter, *http.Request) error
Далее, чтобы возвращать ошибки, мы можем изменить нашу функцию viewRecord:
func viewRecord(w http.ResponseWriter, r *http.Request) error {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        return err
    }
    return viewTemplate.Execute(w, record)
}
Это проще, чем был в изначальной версия, но пакет http не понимает функции, которые возвращают error. Чтобы это исправить, мы может реализовать интерфейс http.Handler метод ServeHTTP для appHandler:
func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if err := fn(w, r); err != nil {
        http.Error(w, err.Error(), 500)
    }
}
Метод ServeHTTP вызывает функцию appHandler и показывает возвращенную ошибку. Обратите внимание, что получателем метода fn является функция. (Go может такое!) Метод запускает функцию вызывая выражение fn(w, r).

Теперь, когда регистрируется viewRecord, мы с пакетом http используем функцию Handle (вместо HandleFunc) как appHandler, которая является http.Handler (не http.HandlerFunc).
func init() {
    http.Handle("/view", appHandler(viewRecord))
}
С такой базовой инфраструктурой, мы можем сделать обработку ошибок более удобной для использования. Вместо того, чтобы просто вывести на экран монитора строку с ошибкой, было бы лучше дать пользователю простое сообщение об ошибке с адекватным кодом состояния HTTP, в то время как полную ошибку, с целью устранения неисправности, зарегистрировать в консоли разработчика App Engine.

Чтобы это сделать, мы создадим структуру appError, которая содержит error и некоторые другие поля:
type appError struct {
    Error   error
    Message string
    Code    int
}
Затем, мы изменяем тип appHandler, чтобы он возвращать значения *appError:
type appHandler func(http.ResponseWriter, *http.Request) *appError
(Обычно возвращается конкретный тип ошибки вместо error, о причине этого говорится в Go FAQ, но здесь это правильное решение, потому что ServeHTTP только видит значение и использует содержание.)

Метод ServeHTTP принадлежащий “appHandler” выводит Message “appError”, чтобы использовать с корректным HTTP состоянием Code и регистрацией в журнале консоли разработчика всей Error:
func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if e := fn(w, r); e != nil { // e - это*appError, а не os.Error.
        c := appengine.NewContext(r)
        c.Errorf("%v", e.Error)
        http.Error(w, e.Message, e.Code)
    }
}
В заключение, мы обновим viewRecord, чтобы новая сигнатура функции и её возврат, больше подходили контексту, когда встретится ошибка:
func viewRecord(w http.ResponseWriter, r *http.Request) *appError {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        return &appError{err, "Record not found", 404}
    }
    if err := viewTemplate.Execute(w, record); err != nil {
        return &appError{err, "Can't display record", 500}
    }
    return nil
}
Эта версия viewRecord большей частью такая же как оригинал, но теперь у каждой строки есть определенное значение и мы обеспечиваем более дружественное использование.

Обработка ошибок этим не заканчивается. Мы можем улучшить её в нашем приложении. Некоторые идеи:

- Дайте обработчику ошибок хороший шаблон HTML.

- Сделайте отладку проще, записывая трассировку стека при реакции HTTP, когда пользователь является администратором.

- Напишите функцию-конструктор для appError, чтобы сохранять трассировку стека для более простой отладки.

- recover из panics внутри appHandler, регистрируйте ошибку в консоли как “Критическая”, говоря пользователю о серьезности возникшей ошибки. Это хорошая практика, чтобы избавлять пользователя от загадочных сообщений об ошибке, вызванной в результате ошибки программирования. Детали в статье “Defer, Panic и Recover”.

Заключение


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


© Перевод: www.golangpro.ru

Комментариев нет:

Отправить комментарий