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)
}
Вывод должен выглядеть так:
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()
явно, чтобы получить строку "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()
.
Интерфейсы обеспечивают узкое представление о широком спектре методов, предоставляемых типами, поэтому мы должны использовать утверждение типа, чтобы изменить методы, который отображает это представление, или полностью удалить его.
Следующий пример дополняет пример с 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"
. На практике мы будем выполнять другой запрос, а не выводить сообщение.
Как правило, ошибка будет генерироваться где-то за пределами вашей программы, например в базе данных, сетевом подключении и т. д. Сообщения об ошибке, возникающие в результате этих ошибок, не в состоянии помочь кому-либо найти причину ошибки. Оборачивание ошибки в дополнительную информацию в начале сообщения об ошибке позволит предоставить необходимый контекст для успешной отладки.
Следующий пример показывает, как мы можем привязать некоторую сопроводительную информацию к какой-либо непонятной ошибке
, возвращаемой одной из функций:
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
— это структура с двумя полями: контекстное сообщение, например, в виде строки
, и ошибка
, о которой WrappedError
предоставляет дополнительную информацию. Когда вызывается метод Error()
, мы снова будем использовать fmt.Sprintf
для вывода контекстного сообщения, а затем error
(fmt.Sprintf
также неявно вызывает метод Error()
).
Внутри main()
мы создаем ошибку, используя errors.New
, а затем оборачиваем эту ошибку, используя определенную нами функцию Wrap
. Это позволяет нам указать, что эта ошибка
была сгенерирована в методе "main"
. Также, поскольку наша WrappedError
также является ошибкой
, мы можем обернуть другие ошибки WrappedError
, что позволит нам посмотреть цепочку, чтобы мы могли отследить источник ошибки. При небольшой помощи стандартной библиотеки мы можем даже ввести полноценную трассировку стека для наших ошибок.
Поскольку интерфейс error
имеет всего один метод, мы видим, что нам предоставлена большая гибкость при создании разных типов ошибок для разных ситуаций. Это позволяет охватить все, передавая самые разные куски информации в качестве части ошибки, а также реализовать экспоненциальную задержку. Хотя механизмы обработки ошибки в Go могут выглядеть простейшими, мы можем добиться очень сложной обработки с помощью этих настраиваемых ошибок для стандартных и нестандартных ситуаций.
В Go есть другой механизм для передачи неожиданного поведения — паники. В нашей следующей статье, посвященной работе с ошибками, мы рассмотрим паники и узнаем, что это такое и как с ними работать.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!