Создание пользовательских ошибок в Go

Вступление

Go предоставляет два метода для создания ошибок в стандартной библиотекеerrors.New and 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)
}

Мы увидим следующий вывод:

Outputunexpected 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()transparently для получения строки"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!")
}

Мы увидим следующий вывод:

Outputstatus 503: err unavailable
exit status 1

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

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

Введите утверждения и пользовательские ошибки

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

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

В следующем примере показано, что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!")
}

Мы увидим следующий вывод:

Outputunavailable
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". На практике мы вместо этого делаем другой запрос, а не печатаем сообщение.

Ошибки упаковки

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

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

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)
}

Мы увидим следующий вывод:

Outputmain: boom!

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

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

Заключение

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

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