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