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

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

Читайте также: Обработка ошибок в Go

Чтобы продуктивно обработать эту более сложную информацию, можно использовать тип интерфейса error из стандартной библиотеки.

Его синтаксис выглядит так:

type error interface {
Error() string        
}

Пакет builtin определяет 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)
}

Мы получим такой вывод:

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

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

Вы получите такой вывод:

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

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

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

В выводе будет:

main: boom!

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

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

Заключение

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

в Go есть еще один механизм для сообщения о неожиданном поведении – это panics. Мы рассмотрим его в одной из будущих статей.

Tags: ,