Welcome to tutorial no. 31 in our Golang tutorial series.
In the last tutorial we learnt about error representation in Go and how to handle errors from the standard library. We also learnt how to extract more information from the errors.
This tutorial deals with how to create our own custom errors which we can use in our functions and packages. We will also use the same techniques employed by the standard library to provide more details about our custom errors.
Creating custom errors using the New function
The simplest way to create a custom error is to use the New function of the errors package.
Before we use the New function to create a custom error, let’s understand how it is implemented. The implementation of the New function in the errors package is provided below.
package errors
// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
return &errorString{text}
}
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
The implementation is pretty simple. errorString
is a struct type with a single string field s
. The Error() string
method of the error
interface is implemented using a errorString
pointer receiver in line no. 14.
The New
function in line no. 5 takes a string
parameter, creates a value of type errorString
using that parameter and returns the address of it. Thus a new error is created and returned.
Now that we know how the New
function works, lets use it in a program of our own to create a custom error.
We will create a simple program which calculates the area of a circle and will return an error if the radius is negative.
package main
import (
"errors"
"fmt"
"math"
)
func circleArea(radius float64) (float64, error) {
if radius < 0 {
return 0, errors.New("Area calculation failed, radius is less than zero")
}
return math.Pi * radius * radius, nil
}
func main() {
radius := -20.0
area, err := circleArea(radius)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("Area of circle %0.2f", area)
}
Run in playground
In the program above, we check whether the radius is less than zero in line no. 10. If so we return zero for the area along with the corresponding error message. If the radius is greater than 0, then the area is calculated and nil
is returned as the error in line no. 13.
In the main function, we check whether the error is not nil
in line no. 19. If it’s not nil
, we print the error and return, else the area of the circle is printed.
In this program the radius is less than zero and hence it will print,
Area calculation failed, radius is less than zero
Adding more information to the error using Errorf
The above program works well but wouldn’t it be nice if we print the actual radius which caused the error. This is where the Errorf function of the fmt package comes in handy. This function formats the error according to a format specifier and returns a string as value that satisfies the error
interface.
Let’s use the Errorf
function and make the program better.
package main
import (
"fmt"
"math"
)
func circleArea(radius float64) (float64, error) {
if radius < 0 {
return 0, fmt.Errorf("Area calculation failed, radius %0.2f is less than zero", radius)
}
return math.Pi * radius * radius, nil
}
func main() {
radius := -20.0
area, err := circleArea(radius)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("Area of circle %0.2f", area)
}
Run in playground
In the program above, the Errorf
is used in line no. 10 to print the actual radius which caused the error. Running this program will output,
Area calculation failed, radius -20.00 is less than zero
Providing more information about the error using struct type and fields
It is also possible to use struct types which implement the error interface as errors. This gives us more flexibility with error handling. In our previous example, if we want to access the radius which caused the error, the only way now is to parse the error description Area calculation failed, radius -20.00 is less than zero
. This is not a proper way to do this since if the description changes, our code will break.
We will use the strategy followed by the standard library explained in the previous tutorial under the section «Converting the error to the underlying type and retrieving more information from the struct fields» and use struct fields to provide access to the radius which caused the error. We will create a struct type that implements the error interface and use its fields to provide more information about the error.
The first step would be create a struct type to represent the error. The naming convention for error types is that the name should end with the text Error
. So let’s name our struct type as areaError
type areaError struct {
err string
radius float64
}
The above struct type has a field radius
which stores the value of the radius responsible for the error and err
field stores the actual error message.
The next step is to implement the error interface.
func (e *areaError) Error() string {
return fmt.Sprintf("radius %0.2f: %s", e.radius, e.err)
}
In the above snippet, we implement the Error() string
method of the error interface using a pointer receiver *areaError
. This method prints the radius and the error description.
Let’s complete the program by writing the main
function and circleArea
function.
package main
import (
"errors"
"fmt"
"math"
)
type areaError struct {
err string
radius float64
}
func (e *areaError) Error() string {
return fmt.Sprintf("radius %0.2f: %s", e.radius, e.err)
}
func circleArea(radius float64) (float64, error) {
if radius < 0 {
return 0, &areaError{
err: "radius is negative",
radius: radius,
}
}
return math.Pi * radius * radius, nil
}
func main() {
radius := -20.0
area, err := circleArea(radius)
if err != nil {
var areaError *areaError
if errors.As(err, &areaError) {
fmt.Printf("Area calculation failed, radius %0.2f is less than zero", areaError.radius)
return
}
fmt.Println(err)
return
}
fmt.Printf("Area of rectangle %0.2f", area)
}
Run in playground
In the program above, circleArea
in line no. 18 is used to calculate the area of the circle. This function first checks if the radius is less than zero, if so it creates a value of type areaError
using the radius responsible for the error and the corresponding error message and then returns the address of it in line no. 20 along with 0
as area. Thus we have provided more information about the error, in this case the radius which caused the error using the fields of a custom error struct.
If the radius is not negative, this function calculates and returns the area along with a nil
error in line no. 25.
In line no. 30 of the main function, we are trying to find the area of a circle with radius -20. Since the radius is less than zero, an error will be returned.
We check whether the error is not nil
in line no. 31 and in line no. 33 line we try to convert it to type *areaError
. If the error is of type *areaError
, we get the radius which caused the error in line no. 34 using areaError.radius
, print a custom error message and return from the program.
If the error is not of type *areaError
, we simply print the error in line no. 37 and return. If there is no error, the area will be printed in line no.40.
The program will print,
Area calculation failed, radius -20.00 is less than zero
Now lets use the second strategy described in the previous tutorial and use methods on custom error types to provide more information about the error.
Providing more information about the error using methods on struct types
In this section we will write a program which calculates the area of a rectangle. This program will print an error if either the length or width is less than zero.
The first step would be create a struct to represent the error.
type areaError struct {
err string //error description
length float64 //length which caused the error
width float64 //width which caused the error
}
The above error struct type contains an error description field along with the length and width which caused the error.
Now that we have the error type, lets implement the error interface and add a couple of methods on the error type to provide more information about the error.
func (e *areaError) Error() string {
return e.err
}
func (e *areaError) lengthNegative() bool {
return e.length < 0
}
func (e *areaError) widthNegative() bool {
return e.width < 0
}
In the above snippet, we return the description of the error from the Error() string
method. The lengthNegative() bool
method returns true when the length is less than zero and widthNegative() bool
method returns true when the width is less than zero. These two methods provide more information about the error, in this case they say whether the area calculation failed because of the length being negative or width being negative. Thus we have used methods on struct error types to provide more information about the error.
The next step is to write the area calculation function.
func rectArea(length, width float64) (float64, error) {
err := ""
if length < 0 {
err += "length is less than zero"
}
if width < 0 {
if err == "" {
err = "width is less than zero"
} else {
err += ", width is less than zero"
}
}
if err != "" {
return 0, &areaError{
err: err,
length: length,
width: width,
}
}
return length * width, nil
}
The rectArea
function above checks if either the length or width is less than zero, if so it returns an error of type *areaError
, else it returns the area of the rectangle with nil
as error.
Let’s finish this program by creating the main function.
func main() {
length, width := -5.0, -9.0
area, err := rectArea(length, width)
if err != nil {
var areaError *areaError
if errors.As(err, &areaError) {
if areaError.lengthNegative() {
fmt.Printf("error: length %0.2f is less than zeron", areaError.length)
}
if areaError.widthNegative() {
fmt.Printf("error: width %0.2f is less than zeron", areaError.width)
}
return
}
fmt.Println(err)
return
}
fmt.Println("area of rect", area)
}
In the main function, we check whether the error is not nil
in line no. 4. If it is not nil, we try to convert it to type *areaError
. Then using the lengthNegative()
and widthNegative()
methods, we check whether the error is because of the fact that the length is negative or width is negative. We print the corresponding error message and return from the program. Thus we have used the methods on the error struct type to provide more information about the error.
If there is no error, the area of the rectangle will be printed.
Here is the full program for your reference.
package main
import (
"errors"
"fmt"
)
type areaError struct {
err string //error description
length float64 //length which caused the error
width float64 //width which caused the error
}
func (e *areaError) Error() string {
return e.err
}
func (e *areaError) lengthNegative() bool {
return e.length < 0
}
func (e *areaError) widthNegative() bool {
return e.width < 0
}
func rectArea(length, width float64) (float64, error) {
err := ""
if length < 0 {
err += "length is less than zero"
}
if width < 0 {
if err == "" {
err = "width is less than zero"
} else {
err += ", width is less than zero"
}
}
if err != "" {
return 0, &areaError{
err: err,
length: length,
width: width,
}
}
return length * width, nil
}
func main() {
length, width := -5.0, -9.0
area, err := rectArea(length, width)
if err != nil {
var areaError *areaError
if errors.As(err, &areaError) {
if areaError.lengthNegative() {
fmt.Printf("error: length %0.2f is less than zeron", areaError.length)
}
if areaError.widthNegative() {
fmt.Printf("error: width %0.2f is less than zeron", areaError.width)
}
return
}
fmt.Println(err)
return
}
fmt.Println("area of rect", area)
}
Run in playground
This program will print the output,
error: length -5.00 is less than zero
error: width -9.00 is less than zero
We have seen examples for two of the three ways described in the error handling tutorial to provide more information about the errors.
The third way using direct comparison is pretty straightforward. I would leave it as an exercise for you to figure out how to use this strategy to provide more information about our custom errors.
This brings us to an end of this tutorial.
Here is a quick recap of what we learnt in this tutorial,
- Creating custom errors using the New function
- Adding more information to the error using Errorf
- Providing more information about the error using struct type and fields
- Providing more information about the error using methods on struct types
Have a good day.
Next tutorial — Panic and Recover
11 октября, 2019 12:10 пп
619 views
| Комментариев нет
Cloud Server
В стандартной библиотеке 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: Go, Golang
yourbasic.org/golang
String-based errors
The standard library offers two out-of-the-box options.
// simple string-based error
err1 := errors.New("math: square root of negative number")
// with formatting
err2 := fmt.Errorf("math: square root of negative number %g", x)
Custom errors with data
To define a custom error type, you must satisfy the predeclared error
interface.
type error interface {
Error() string
}
Here are two examples.
type SyntaxError struct {
Line int
Col int
}
func (e *SyntaxError) Error() string {
return fmt.Sprintf("%d:%d: syntax error", e.Line, e.Col)
}
type InternalError struct {
Path string
}
func (e *InternalError) Error() string {
return fmt.Sprintf("parse %v: internal error", e.Path)
}
If Foo
is a function that can return a SyntaxError
or an InternalError
, you may handle the two cases like this.
if err := Foo(); err != nil {
switch e := err.(type) {
case *SyntaxError:
// Do something interesting with e.Line and e.Col.
case *InternalError:
// Abort and file an issue.
default:
log.Println(e)
}
}
More code examples
Go blueprints: code for common tasks is a collection of handy code examples.
Share this page:
- 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)
}
Мы увидим следующий вывод:
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 есть другой механизм для сообщения о неожиданном поведении, панике. В нашей следующей статье из серии обработки ошибок мы рассмотрим панику — что это такое и как с ними справляться.
Александр Тихоненко
Ведущий разработчик трайба «Автоматизация бизнес-процессов» МТС Диджитал
Механизм обработки ошибок в Go отличается от обработки исключений в большинстве языков программирования, ведь в Golang ошибки исключениями не являются. Если говорить в целом, то ошибка в Go — это возвращаемое значение с типомerror
, которое демонстрирует сбой. А с точки зрения кода — интерфейс. В качестве ошибки может выступать любой объект, который этому интерфейсу удовлетворяет.
Выглядит это так:
type error interface {
Error() string
}
В данной статье мы рассмотрим наиболее популярные способы работы с ошибками в Golang.
- Как обрабатывать ошибки в Go?
- Создание ошибок
- Оборачивание ошибок
- Проверка типов с Is и As
- Сторонние пакеты по работе с ошибками в Go
- Defer, panic and recover
- После изложенного
Чтобы обработать ошибку в Golang, необходимо сперва вернуть из функции переменную с объявленным типом error
и проверить её на nil
:
if err != nil {
return err
}
Если метод возвращает ошибку, значит, потенциально в его работе может возникнуть проблема, которую нужно обработать. В качестве реализации обработчика может выступать логирование ошибки или более сложные сценарии. Например, переоткрытие установленного сетевого соединения, повторный вызов метода и тому подобные операции.
Если метод возвращает разные типы ошибок, то их нужно проверять отдельно. То есть сначала происходит определение ошибки, а потом для каждого типа пишется свой обработчик.
В Go ошибки возвращаются и проверяются явно. Разработчик сам определяет, какие ошибки метод может вернуть, и реализовать их обработку на вызывающей стороне.
Создание ошибок
Перед тем как обработать ошибку, нужно её создать. В стандартной библиотеке для этого есть две встроенные функции — обе позволяют указывать и отображать сообщение об ошибке:
errors.New
fmt.Errorf
Метод errors.New()
создаёт ошибку, принимая в качестве параметра текстовое сообщение.
package main
import (
"errors"
"fmt"
)
func main() {
err := errors.New("emit macho dwarf: elf header corrupted")
fmt.Print(err)
}
С помощью метода fmt.Errorf
можно добавить дополнительную информацию об ошибке. Данные будут храниться внутри одной конкретной строки.
package main
import (
"fmt"
)
func main() {
const name, id = "bueller", 17
err := fmt.Errorf("user %q (id %d) not found", name, id)
fmt.Print(err)
}
Такой способ подходит, если эта дополнительная информация нужна только для логирования на вызывающей стороне. Если же с ней предстоит работать, можно воспользоваться другими механизмами.
Оборачивание ошибок
Поскольку Error
— это интерфейс, можно создать удовлетворяющую ему структуру с собственными полями. Тогда на вызывающей стороне этими самыми полями можно будет оперировать.
package main
import (
"fmt"
)
type NotFoundError struct {
UserId int
}
func (err NotFoundError) Error() string {
return fmt.Sprintf("user with id %d not found", err.UserId)
}
func SearchUser(id int) error {
// some logic for search
// ...
// if not found
var err NotFoundError
err.UserId = id
return err
}
func main() {
const id = 17
err := SearchUser(id)
if err != nil {
fmt.Println(err)
//type error checking
notFoundErr, ok := err.(NotFoundError)
if ok {
fmt.Println(notFoundErr.UserId)
}
}
}
Представим другую ситуацию. У нас есть метод, который вызывает внутри себя ещё один метод. В каждом из них проверяется своя ошибка. Иногда требуется в метод верхнего уровня передать сразу обе эти ошибки.
В Go есть соглашение о том, что ошибка, которая содержит внутри себя другую ошибку, может реализовать метод Unwrap
, который будет возвращать исходную ошибку.
Также для оборачивания ошибок в fmt.Errorf
есть плейсхолдер %w
, который и позволяет произвести такую упаковку.:
package main
import (
"errors"
"fmt"
"os"
)
func main() {
err := openFile("non-existing")
if err != nil {
fmt.Println(err.Error())
// get internal error
fmt.Println(errors.Unwrap(err))
}
}
func openFile(filename string) error {
if _, err := os.Open(filename); err != nil {
return fmt.Errorf("error opening %s: %w", filename, err)
}
return nil
}
Проверка типов с Is и As
В Go 1.13 в пакете Errors появились две функции, которые позволяют определить тип ошибки — чтобы написать тот или иной обработчик:
errors.Is
errors.As
Метод errors.Is
, по сути, сравнивает текущую ошибку с заранее заданным значением ошибки:
package main
import (
"errors"
"fmt"
"io/fs"
"os"
)
func main() {
if _, err := os.Open("non-existing"); err != nil {
if errors.Is(err, fs.ErrNotExist) {
fmt.Println("file does not exist")
} else {
fmt.Println(err)
}
}
}
Если это будет та же самая ошибка, то функция вернёт true
, если нет — false
.
errors.As
проверяет, относится ли ошибка к конкретному типу (раньше надо было явно приводить тип ошибки к тому типу, который хотим проверить):
package main
import (
"errors"
"fmt"
"io/fs"
"os"
)
func main() {
if _, err := os.Open("non-existing"); err != nil {
var pathError *fs.PathError
if errors.As(err, &pathError) {
fmt.Println("Failed at path:", pathError.Path)
} else {
fmt.Println(err)
}
}
}
Помимо прочего, эти методы удобны тем, что упрощают работу с упакованными ошибками, позволяя проверить каждую из них за один вызов.
Сторонние пакеты по работе с ошибками в Go
Помимо стандартного пакета Go, есть различные внешние библиотеки, которые расширяют функционал. При принятии решения об их использовании следует отталкиваться от задачи — использование может привести к падению производительности.
В качестве примера можно посмотреть на пакет pkg/errors
. Одной из его способностей является логирование stack trace:
package main
import (
"fmt"
"github.com/pkg/errors"
)
func main() {
err := errors.Errorf("whoops: %s", "foo")
fmt.Printf("%+v", err)
}
// Example output:
// whoops: foo
// github.com/pkg/errors_test.ExampleErrorf
// /home/dfc/src/github.com/pkg/errors/example_test.go:101
// testing.runExample
// /home/dfc/go/src/testing/example.go:114
// testing.RunExamples
// /home/dfc/go/src/testing/example.go:38
// testing.(*M).Run
// /home/dfc/go/src/testing/testing.go:744
// main.main
// /github.com/pkg/errors/_test/_testmain.go:102
// runtime.main
// /home/dfc/go/src/runtime/proc.go:183
// runtime.goexit
// /home/dfc/go/src/runtime/asm_amd64.s:2059
Defer, panic and recover
Помимо ошибок, о которых позаботился разработчик, в Go существуют аварии (похожи на исключительные ситуации, например, в Java). По сути, это те ошибки, которые разработчик не предусмотрел.
При возникновении таких ошибок Go останавливает выполнение программы и начинает раскручивать стек вызовов до тех пор, пока не завершит работу приложения или не найдёт функцию обработки аварии.
Для работы с такими ошибками существует механизм «defer, panic, recover»
Defer
Defer
помещает все вызовы функции в стек приложения. При этом отложенные функции выполняются в обратном порядке — независимо от того, вызвана паника или нет. Это бывает полезно при очистке ресурсов:
package main
import (
"fmt"
"os"
)
func main() {
f := createFile("/tmp/defer.txt")
defer closeFile(f)
writeFile(f)
}
func createFile(p string) *os.File {
fmt.Println("creating")
f, err := os.Create(p)
if err != nil {
panic(err)
}
return f
}
func writeFile(f *os.File) {
fmt.Println("writing")
fmt.Fprintln(f, "data")
}
func closeFile(f *os.File) {
fmt.Println("closing")
err := f.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "error: %vn", err)
os.Exit(1)
}
}
Panic
Panic
сигнализирует о том, что код не может решить текущую проблему, и останавливает выполнение приложения. После вызова оператора выполняются все отложенные функции, и программа завершается с сообщением о причине паники и трассировки стека.
Например, Golang будет «паниковать», когда число делится на ноль:
panic: runtime error: integer divide by zero
goroutine 1 [running]:
main.divide(0x0)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:16 +0xe6
main.divide(0x1)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x2)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x3)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x4)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x5)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.main()
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:11 +0x31
exit status 2
Также панику можно вызвать явно с помощью метода panic()
. Обычно его используют на этапе разработки и тестирования кода — а в конечном варианте убирают.
Recover
Эта функция нужна, чтобы вернуть контроль при панике. В таком случае работа приложения не прекращается, а восстанавливается и продолжается в нормальном режиме.
Recover всегда должна вызываться в функции defer
. Чтобы сообщить об ошибке как возвращаемом значении, вы должны вызвать функцию recover в той же горутине, что и паника, получить структуру ошибки из функции восстановления и передать её в переменную:
package main
import (
"errors"
"fmt"
)
func A() {
defer fmt.Println("Then we can't save the earth!")
defer func() {
if x := recover(); x != nil {
fmt.Printf("Panic: %+vn", x)
}
}()
B()
}
func B() {
defer fmt.Println("And if it keeps getting hotter...")
C()
}
func C() {
defer fmt.Println("Turn on the air conditioner...")
Break()
}
func Break() {
defer fmt.Println("If it's more than 30 degrees...")
panic(errors.New("Global Warming!!!"))
}
func main() {
A()
}
После изложенного
Можно ли игнорировать ошибки? В теории — да. Но делать это нежелательно. Во-первых, наличие ошибки позволяет узнать, успешно ли выполнился метод. Во-вторых, если метод возвращает полезное значение и ошибку, то, не проверив её, нельзя утверждать, что полезное значение корректно.
Надеемся, приведённые методы обработки ошибок в Go будут вам полезны. Читайте также статью о 5 главных ошибках Junior-разработчика, чтобы не допускать их в начале своего карьерного пути.