Введение

Go предоставляет два способа создания ошибок в стандартной библиотеке, errors.New и fmt.Errorf. При передаче более сложной информации об ошибках для ваших пользователей или для собственного использования в будущем во время отладки может случиться, что этих двух механизмов будет недостаточно для надлежащего сбора данных и вывода информации о том, что произошло. Чтобы передавать эту более подробную информацию и реализовать дополнительный функционал, мы можем реализовать стандартный тип интерфейса библиотеки — error.

Синтаксис в этом случае будет выглядеть следующим образом:

type error interface {
  Error() string
}

Пакет builtin определяет error как интерфейс с единственным методом Error()​​​, который возвращает сообщение об ошибке в виде строки. При реализации этого метода мы можем преобразовать любой тип, который мы определяем в качестве нашей собственной ошибки.

Давайте попробуем запустить следующий пример, чтобы увидеть реализацию интерфейса error:

package main

import (
    "fmt"
    "os"
)

type MyError struct{}

func (m *MyError) Error() string {
    return "boom"
}

func sayHello() (string, error) {
    return "", &MyError{}
}

func main() {
    s, err := sayHello()
    if err != nil {
        fmt.Println("unexpected error: err:", err)
        os.Exit(1)
    }
    fmt.Println("The string:", s)
}

Вывод должен выглядеть так:

Output
unexpected error: err: boom exit status 1

Здесь мы создали новый пустой тип структуры, MyError, и определили в нем метод Error(). Метод Error() возвращает строку "boom".

Внутри main() мы вызываем функцию sayHello, которая возвращает пустую строку и новый экземпляр MyError. Поскольку sayHello всегда будет возвращать ошибку, вызов fmt.Println внутри тела оператора if в main() будет выполняться всегда. Затем мы используем fmt.Println для вывода короткого префикса "unexpected error:" вместе с экземпляром MyError, который хранится внутри переменной err.

Обратите внимание, что нам не нужно напрямую вызывать Error(), поскольку пакет fmt может автоматически обнаруживать, что это реализация error. Он вызывает Error() явно, чтобы получить строку "boom" и выполняет конкатенацию со строкой префикса "unexpected error: err:".

Сбор подробной информации в настраиваемой ошибке

Иногда настраиваемая ошибка является самым понятным способом получения подробной информации об ошибке. Например, скажем, мы хотим получать код статуса для ошибок, генерируемых HTTP-запросом; запустите следующую программу, чтобы посмотреть на реализацию error, которая позволяет нам получать эту информацию:

package main

import (
    "errors"
    "fmt"
    "os"
)

type RequestError struct {
    StatusCode int

    Err error
}

func (r *RequestError) Error() string {
    return fmt.Sprintf("status %d: err %v", r.StatusCode, r.Err)
}

func doRequest() error {
    return &RequestError{
        StatusCode: 503,
        Err:        errors.New("unavailable"),
    }
}

func main() {
    err := doRequest()
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    fmt.Println("success!")
}

Вывод будет выглядеть следующим образом:

Output
status 503: err unavailable exit status 1

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

Внутри метода Error() в RequestError мы используем функцию fmt.Sprintf для создания строки с информацией, предоставляемой при создании ошибки.

Утверждение типа и настраиваемые ошибки

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

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

Следующий пример дополняет пример с RequestError, показанный ранее, и демонстрирует метод Temporary(), который будет указывать, должны ли вызывающие повторять запрос:

package main

import (
    "errors"
    "fmt"
    "net/http"
    "os"
)

type RequestError struct {
    StatusCode int

    Err error
}

func (r *RequestError) Error() string {
    return r.Err.Error()
}

func (r *RequestError) Temporary() bool {
    return r.StatusCode == http.StatusServiceUnavailable // 503
}

func doRequest() error {
    return &RequestError{
        StatusCode: 503,
        Err:        errors.New("unavailable"),
    }
}

func main() {
    err := doRequest()
    if err != nil {
        fmt.Println(err)
        re, ok := err.(*RequestError)
        if ok {
            if re.Temporary() {
                fmt.Println("This request can be tried again")
            } else {
                fmt.Println("This request cannot be tried again")
            }
        }
        os.Exit(1)
    }

    fmt.Println("success!")
}

Вывод будет выглядеть следующим образом:

Output
unavailable This request can be tried again exit status 1

Внутри main() мы вызываем doRequest(), метод, возвращающий нам интерфейс error. Сначала мы выводим сообщение об ошибке, возвращаемое методом Error(). Далее мы попробуем открыть все методы RequestError, используя утверждение типов re, ok := err.( *RequestError). Если утверждение типа будет выполнено успешно, мы будем использовать метод Temporary(), чтобы убедиться, что эта ошибка является временной ошибкой. Поскольку StatusCode, заданный doRequest(), равен 503, что соответствует http.StatusServiceUnavailable, будет возвращено значение true, а на экран будет выведена причина "This request can be tried again"​​​. На практике мы будем выполнять другой запрос, а не выводить сообщение.

Обертка для ошибок

Как правило, ошибка будет генерироваться где-то за пределами вашей программы, например в базе данных, сетевом подключении и т. д. Сообщения об ошибке, возникающие в результате этих ошибок, не в состоянии помочь кому-либо найти причину ошибки. Оборачивание ошибки в дополнительную информацию в начале сообщения об ошибке позволит предоставить необходимый контекст для успешной отладки.

Следующий пример показывает, как мы можем привязать некоторую сопроводительную информацию к какой-либо непонятной ошибке, возвращаемой одной из функций:

package main

import (
    "errors"
    "fmt"
)

type WrappedError struct {
    Context string
    Err     error
}

func (w *WrappedError) Error() string {
    return fmt.Sprintf("%s: %v", w.Context, w.Err)
}

func Wrap(err error, info string) *WrappedError {
    return &WrappedError{
        Context: info,
        Err:     err,
    }
}

func main() {
    err := errors.New("boom!")
    err = Wrap(err, "main")

    fmt.Println(err)
}

Вывод будет выглядеть следующим образом:

Output
main: boom!

WrappedError — это структура с двумя полями: контекстное сообщение, например, в виде строки, и ошибка, о которой WrappedError предоставляет дополнительную информацию. Когда вызывается метод Error(), мы снова будем использовать fmt.Sprintf для вывода контекстного сообщения, а затем error (fmt.Sprintf также неявно вызывает метод Error()).

Внутри main() мы создаем ошибку, используя errors.New, а затем оборачиваем эту ошибку, используя определенную нами функцию Wrap. Это позволяет нам указать, что эта ошибка была сгенерирована в методе "main". Также, поскольку наша WrappedError также является ошибкой, мы можем обернуть другие ошибки WrappedError, что позволит нам посмотреть цепочку, чтобы мы могли отследить источник ошибки. При небольшой помощи стандартной библиотеки мы можем даже ввести полноценную трассировку стека для наших ошибок.

Заключение

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

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

0 Comments

Creative Commons License