Как создать ошибку golang

Строковые ошибки

Стандартная библиотека предлагает два готовых варианта.

// простая строковая ошибка
err1 := errors.New("math: square root of negative number")

// с форматированием
err2 := fmt.Errorf("math: square root of negative number %g", x)

Пользовательские ошибки с данными

Чтобы определить пользовательский тип ошибки, вы должны реализовать предварительно объявленный интерфейс error.

type error interface {
    Error() string
}

Вот два примера.

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

Если Foo является функцией, которая может возвращать SyntaxError или InternalError, вы можете обрабатывать два случая, как показано ниже:

if err := Foo(); err != nil {
    switch e := err.(type) {
    case *SyntaxError:
        // Делаем что-нибудь интересное с e.Line и e.Col.
    case *InternalError:
        // Прервать и записать проблему.
    default:
        log.Println(e)
    }
}

Читайте также:

  • Работа с ошибками в Go 1.13
  • Основы Go: ошибки
  • Эффективный Go: ошибки
  • Обработка ошибок в Golang

Package errors implements functions to manipulate errors.

The New function creates errors whose only content is a text message.

An error e wraps another error if e’s type has one of the methods

Unwrap() error
Unwrap() []error

If e.Unwrap() returns a non-nil error w or a slice containing w,
then we say that e wraps w. A nil error returned from e.Unwrap()
indicates that e does not wrap any error. It is invalid for an
Unwrap method to return an []error containing a nil error value.

An easy way to create wrapped errors is to call fmt.Errorf and apply
the %w verb to the error argument:

wrapsErr := fmt.Errorf("... %w ...", ..., err, ...)

Successive unwrapping of an error creates a tree. The Is and As
functions inspect an error’s tree by examining first the error
itself followed by the tree of each of its children in turn
(pre-order, depth-first traversal).

Is examines the tree of its first argument looking for an error that
matches the second. It reports whether it finds a match. It should be
used in preference to simple equality checks:

if errors.Is(err, fs.ErrExist)

is preferable to

if err == fs.ErrExist

because the former will succeed if err wraps io/fs.ErrExist.

As examines the tree of its first argument looking for an error that can be
assigned to its second argument, which must be a pointer. If it succeeds, it
performs the assignment and returns true. Otherwise, it returns false. The form

var perr *fs.PathError
if errors.As(err, &perr) {
	fmt.Println(perr.Path)
}

is preferable to

if perr, ok := err.(*fs.PathError); ok {
	fmt.Println(perr.Path)
}

because the former will succeed if err wraps an *io/fs.PathError.

package main

import (
	"fmt"
	"time"
)

// MyError is an error implementation that includes a time and message.
type MyError struct {
	When time.Time
	What string
}

func (e MyError) Error() string {
	return fmt.Sprintf("%v: %v", e.When, e.What)
}

func oops() error {
	return MyError{
		time.Date(1989, 3, 15, 22, 30, 0, 0, time.UTC),
		"the file system has gone away",
	}
}

func main() {
	if err := oops(); err != nil {
		fmt.Println(err)
	}
}
Output:

1989-03-15 22:30:00 +0000 UTC: the file system has gone away
  • Variables
  • func As(err error, target any) bool
  • func Is(err, target error) bool
  • func Join(errs …error) error
  • func New(text string) error
  • func Unwrap(err error) error
  • Package
  • As
  • Is
  • Join
  • New
  • New (Errorf)
  • Unwrap

This section is empty.

View Source

var ErrUnsupported = New("unsupported operation")

ErrUnsupported indicates that a requested operation cannot be performed,
because it is unsupported. For example, a call to os.Link when using a
file system that does not support hard links.

Functions and methods should not return this error but should instead
return an error including appropriate context that satisfies

errors.Is(err, errors.ErrUnsupported)

either by directly wrapping ErrUnsupported or by implementing an Is method.

Functions and methods should document the cases in which an error
wrapping this will be returned.

As finds the first error in err’s tree that matches target, and if one is found, sets
target to that error value and returns true. Otherwise, it returns false.

The tree consists of err itself, followed by the errors obtained by repeatedly
calling Unwrap. When err wraps multiple errors, As examines err followed by a
depth-first traversal of its children.

An error matches target if the error’s concrete value is assignable to the value
pointed to by target, or if the error has a method As(interface{}) bool such that
As(target) returns true. In the latter case, the As method is responsible for
setting target.

An error type might provide an As method so it can be treated as if it were a
different error type.

As panics if target is not a non-nil pointer to either a type that implements
error, or to any interface type.

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

}
Output:

Failed at path: non-existing

Is reports whether any error in err’s tree matches target.

The tree consists of err itself, followed by the errors obtained by repeatedly
calling Unwrap. When err wraps multiple errors, Is examines err followed by a
depth-first traversal of its children.

An error is considered to match a target if it is equal to that target or if
it implements a method Is(error) bool such that Is(target) returns true.

An error type might provide an Is method so it can be treated as equivalent
to an existing error. For example, if MyError defines

func (m MyError) Is(target error) bool { return target == fs.ErrExist }

then Is(MyError{}, fs.ErrExist) returns true. See syscall.Errno.Is for
an example in the standard library. An Is method should only shallowly
compare err and the target and not call Unwrap on either.

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

}
Output:

file does not exist

Join returns an error that wraps the given errors.
Any nil error values are discarded.
Join returns nil if every value in errs is nil.
The error formats as the concatenation of the strings obtained
by calling the Error method of each element of errs, with a newline
between each string.

A non-nil error returned by Join implements the Unwrap() []error method.

package main

import (
	"errors"
	"fmt"
)

func main() {
	err1 := errors.New("err1")
	err2 := errors.New("err2")
	err := errors.Join(err1, err2)
	fmt.Println(err)
	if errors.Is(err, err1) {
		fmt.Println("err is err1")
	}
	if errors.Is(err, err2) {
		fmt.Println("err is err2")
	}
}
Output:

err1
err2
err is err1
err is err2

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.

package main

import (
	"errors"
	"fmt"
)

func main() {
	err := errors.New("emit macho dwarf: elf header corrupted")
	if err != nil {
		fmt.Print(err)
	}
}
Output:

emit macho dwarf: elf header corrupted

The fmt package’s Errorf function lets us use the package’s formatting
features to create descriptive error messages.

package main

import (
	"fmt"
)

func main() {
	const name, id = "bimmler", 17
	err := fmt.Errorf("user %q (id %d) not found", name, id)
	if err != nil {
		fmt.Print(err)
	}
}
Output:

user "bimmler" (id 17) not found

Unwrap returns the result of calling the Unwrap method on err, if err’s
type contains an Unwrap method returning error.
Otherwise, Unwrap returns nil.

Unwrap only calls a method of the form «Unwrap() error».
In particular Unwrap does not unwrap errors returned by Join.

package main

import (
	"errors"
	"fmt"
)

func main() {
	err1 := errors.New("error1")
	err2 := fmt.Errorf("error2: [%w]", err1)
	fmt.Println(err2)
	fmt.Println(errors.Unwrap(err2))
	// Output
	// error2: [error1]
	// error1
}
Output:

This section is empty.

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

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 zero\n", areaError.length)

            }
            if areaError.widthNegative() {
                fmt.Printf("error: width %0.2f is less than zero\n", 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 zero\n", areaError.length)

            }
            if areaError.widthNegative() {
                fmt.Printf("error: width %0.2f is less than zero\n", 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

Рассказываем и показываем на практических примерах, как обрабатывать различные ошибки в языке программирования Go.

Механизм обработки ошибок в Go отличается от обработки исключений в большинстве языков программирования, ведь в Golang ошибки исключениями не являются. Если говорить в целом, то ошибка в Go — это возвращаемое значение с типомerror, которое демонстрирует сбой. А с точки зрения кода — интерфейс. В качестве ошибки может выступать любой объект, который этому интерфейсу удовлетворяет.

Выглядит это так:

			type error interface {  
    Error() string
}
		

В данной статье мы рассмотрим наиболее популярные способы работы с ошибками в Golang.

  1. Как обрабатывать ошибки в Go?
  2. Создание ошибок
  3. Оборачивание ошибок
  4. Проверка типов с Is и As
  5. Сторонние пакеты по работе с ошибками в Go
  6. Defer, panic and recover
  7. После изложенного

Как обрабатывать ошибки в Go?

Чтобы обработать ошибку в 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: %v\n", 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: %+v\n", 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-разработчика, чтобы не допускать их в начале своего карьерного пути.

Понравилась статья? Поделить с друзьями:

Интересное по теме:

  • Как создать ошибку 404 на сайте
  • Как стереть ошибки на мерседесе 210
  • Как сообщить пользователю об ошибке 1с
  • Как стереть ошибку подушки безопасности ниссан кашкай j10
  • Как стереть ошибки камаз 5490

  • 0 0 голоса
    Рейтинг статьи
    Подписаться
    Уведомить о
    guest

    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии