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