пятница, 14 февраля 2020 г.

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

Если вы писали какой-либо код на 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

Вы можете многое сделать в Go, зная только это о типе error, но в этом посте мы более подробно рассмотрим error и обсудим некоторые методы обработки ошибок в Go.

Тип error

Тип error - это тип интерфейса. Переменная error представляет любое значение, которое может быть описано как строка. Вот объявление интерфейса:

type error interface {
    Error() string
}

Тип error, как и для всех встроенных типов, предварительно объявлен в блоке юниверса (universe block).

Наиболее часто используемая реализация error - это неэкспортируемый тип errorString пакета errors.

// errorString это тривиальная реализация error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

Вы можете создать одно из этих значений с помощью функции errors.New. Он принимает строку, которая преобразуется в error.errorString и возвращает значение error.

// New возвращает ошибку, которая форматируется как заданный текст.
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, получает ненулевое значение ошибки (конкретное представление которого является значением errors.errorString). Вызывающая сторона может получить доступ к строке ошибки ("math: square root of..."), вызвав метод Error ошибки или просто распечатав его:

f, err := Sqrt(-1)
if err != nil {
    fmt.Println(err)
}

Пакет fmt форматирует значение ошибки, вызывая его строковый метод Error().

Ответственность за реализацию ошибки заключается в обобщении контекста. Ошибка, возвращаемая форматом os.Open как "open /etc/passwd: permission denied", а не просто "permission denied". Ошибка, возвращаемая нашим Sqrt, содержит информацию о недопустимом аргументе.

Чтобы добавить эту информацию, полезной функцией является Errorf пакета fmt. Она форматирует строку в соответствии с правилами Printf и возвращает ее как ошибку, созданную errors.New.

if f < 0 {
    return 0, fmt.Errorf("math: square root of negative number %g", f)
}

Во многих случаях fmt.Errorf достаточно хорош, но, поскольку 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 возвращает, когда сталкивается с синтаксической ошибкой при разборе BLOB-объекта JSON.

type SyntaxError struct {
    msg    string // описание ошибки
    Offset int64  // ошибка произошла после чтения Offset байтов
}

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
}

Интерфейс 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 и методом Execute viewTemplate. В обоих случаях он представляет пользователю простое сообщение об ошибке с кодом состояния HTTP 500 ("Internal Server Error"). Это похоже на управляемый объем кода, но добавьте еще несколько обработчиков HTTP, и вы быстро получите много копий идентичного кода обработки ошибок.

Чтобы уменьшить количество повторений, мы можем определить наш собственный тип HTTP appHandler, который включает возвращаемое значение ошибки:

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 не понимает функции, которые возвращают ошибку. Чтобы это исправить, мы можем реализовать метод ServeHTTP интерфейса http.Handler в 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, содержащую ошибку и некоторые другие поля:

type appError struct {
    Error   error
    Message string
    Code    int
}

Далее мы модифицируем тип appHandler, чтобы он возвращал значения *appError:

type appHandler func(http.ResponseWriter, *http.Request) *appError

(Обычно является ошибкой возвращать конкретный тип ошибки, а не error, но в данном случае это правильно, потому что ServeHTTP - единственное место, которое видит значение и использует его содержимое.)

И изменяем метод appHandler ServeHTTP, чтобы он отображал сообщение appError для пользователя с правильным кодом состояния HTTP и регистрировал полную ошибку на консоли разработчика:

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 (восстанавливаться) от panic внутри appHandler, записав ошибку на консоль как "Critical" и сообщив пользователю "произошла серьезная ошибка". Это удобно, чтобы не подвергать пользователя непостижимым сообщениям об ошибках, вызванным ошибками программирования.

Заключение

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


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


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

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