ПОЛНОЕ РУКОВОДСТВО

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

Обработка ошибок в Go отличается от других языков программирования, таких как, например, Java или Python. Встроенные ошибки Go не содержат трассировки стека и не поддерживают обычные методы try/catch для их обработки. Вместо этого ошибки в Go — это просто значения, возвращаемые функциями.

Очень хорошая статья об ошибках от Роба Пайка: https://go.dev/blog/errors-are-values

Ошибки можно обрабатывать почти так же, как и любой другой тип данных, что приводит к удивительно легкому и простому дизайну.

Здесь я собираюсь продемонстрировать основы обработки ошибок в Go, а также несколько простых стратегий, которым вы можете следовать в своем коде, чтобы сделать вашу программу надежной и простой в отладке.

Тип ошибки в Go реализован в виде интерфейса следующим образом, что означает, что error — это все, что реализует метод Error(), который возвращает сообщение об ошибке в виде строки.

type error interface {
    Error() string
}

Как сконструировать ошибки

Существует несколько способов создания ошибок, один из которых — создание «на лету» и может быть создан с использованием встроенных в Go пакетов errors или fmt. Например, следующая функция использует пакет errors для возврата новой ошибки со статическим сообщением об ошибке:

package main
import "errors"
func SayHello() error {
    return errors.New("I am unable to speak")
}

Пакет fmt также можно использовать для добавления к ошибке динамических данных, таких как int, string или другой error. Например:

package main
import "fmt"
func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("can't divide '%d' by zero", a)
    }
    return a / b, nil
}

В приведенном выше примере последняя строка возвращает ошибку nil, что означает, что мы возвращаем значение по умолчанию или «нулевое» значение при ошибке. Это важно, поскольку проверка if err != nil — это идиоматический способ определить, была ли обнаружена ошибка.

Когда мы возвращаем ошибку, другие аргументы, возвращаемые функцией, обычно возвращаются как их «нулевое» значение по умолчанию. аналогично, когда для случаев, когда нет ошибки, err возвращается как nil (нулевое значение)

Общее понятие сообщений об ошибках обычно пишется строчными буквами и не заканчивается пунктуацией.

Ожидаемые ошибки

Это достигается путем определения ожидаемых ошибок, чтобы их можно было явно проверить в вызывающем коде, это полезно для выполнения другого набора инструкций, если встречается определенный тип ожидаемой ошибки.

Ошибки часового

Из предыдущего примера сигнализация ошибки функции Divide может быть улучшена путем предварительного определения ошибки «Sentinel». Вызывающие функции могут явно проверять эту ошибку, используя errors.Is

package main
import (
    "errors"
    "fmt"
)
var ErrDivideByZero = errors.New("divide by zero error")
func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, ErrDivideByZero
    }
    return a / b, nil
}
func main() {
    a := 50
    b := 0
    res, err := Divide(a, b)
    if err != nil {
        switch {
        case errors.Is(err, ErrDivideByZero):
            fmt.Println("divide by zero error")
        default:
            fmt.Printf("unexpected error: %s\n", err)
        }
        return
    }
    fmt.Printf("%d / %d = %d\n", a, b, res)
}

Пользовательские типы ошибок

В большинстве случаев описанных выше подходов к обработке ошибок достаточно, однако могут быть случаи, когда вы можете захотеть, чтобы ошибка содержала дополнительные поля данных, или, возможно, сообщение об ошибке должно иметь динамические значения при регистрации.

Это можно сделать в Go, внедрив собственный тип ошибок.

Ниже представлен новый тип DivisionError, который реализует Error interface , как мы видели ранее, теперь он может использовать errors.As для проверки и преобразования стандартной ошибки в более конкретную для нашего случая ошибку DivisionError.

package main
import (
    "errors"
    "fmt"
)
type DivisionError struct {
    A int
    B int
    Msg  string
}
func (e *DivisionError) Error() string { 
    return e.Msg
}
func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, &DivisionError{
            Msg: fmt.Sprintf("cannot divide '%d' by zero", a),
            A: a, B: b,
        }
    }
    return a / b, nil
}
func main() {
    a := 10
    b := 0
    res, err := Divide(a, b)
    if err != nil {
        var divErr *DivisionError
        switch {
        case errors.As(err, &divErr):
            fmt.Printf("%d / %d is not mathematically valid: %s\n",
              divErr.A, divErr.B, divErr.Error())
        default:
            fmt.Printf("unexpected error: %s\n", err)
        }
        return
    }
    fmt.Printf("%d / %d = %d\n", a, b, res)
}

Ошибки переноса

До сих пор ошибки либо создавались, либо возвращались, либо обрабатывались с помощью одного вызова функции. Другими словами, стек функций, участвующих в «пузырьковании» ошибки, имеет глубину всего один уровень.

Часто может быть задействовано гораздо больше функций — от функции, в которой возникает ошибка, до того, где она в конечном итоге обрабатывается, и любого количества дополнительных промежуточных функций.

Существуют такие функции, как errors.Wrap и errors.Unwrap, которые полезны для предоставления дополнительного контекста ошибке, а также для проверки определенных типов ошибок, независимо от того, сколько раз ошибка была перенесена.

Как завернуть ошибку?

Фрагмент ниже показывает пример того, как это сделать, используя fmt.Errorf с глаголом %w для «обертывания» ошибок, поскольку они «пузырятся» через другие вызовы функций, которые добавляют необходимый контекст, чтобы можно было определить, какой из предыдущих вызовов не удался. в стеке вызовов

package main
import (
    "errors"
    "fmt"
    "hey.com/fake/fruits/db"
)
func FetchFruit(fruitName string) (*db.Fruit, error) {
 f, err := db.Find(fruitName)
 if err != nil {
  return nil, fmt.Errorf("FetchFruit: failed executing db query: %w", err)
 }
 return f, nil
}
func SetFruitAge(f *db.Fruit, age int) error {
 if err := db.SetAge(f, age); err != nil {
  return fmt.Errorf("SetFruitAge: failed executing db update: %w", err)
 }
}
func FindAndSetFruitAge(fruitName string, age int) error {
 var fruit *Fruit
 var err error
 fruit, err = FetchFruit(fruitName)
 if err != nil {
  return fmt.Errorf("FindAndSetFruitAge: %w", err)
 }
 if err = SetFruitAge(fruit, age); err != nil {
  return fmt.Errorf("FindAndSetFruitAge: %w", err)
 }
 return nil
}
func main() {
 if err := FindAndSetFruitAge("Banana", 15); err != nil {
  fmt.Println("failed finding or updating fruit: %s", err)
  return
 }
 fmt.Println("successfully updated fruits's age")
}

Если вышеуказанная программа обнаруживает ошибку, в журнале должно быть напечатано следующее:

>_failed finding or updating fruit: FindAndSetFruitAge: SetFruitAge: failed executing db update: malformed request

Теперь сообщение об ошибке содержит информацию о том, что мы видим, что проблема возникла в функции db.SetFruitAge. Это поможет сузить круг ошибок, если сбой произошел после вызова нескольких функций в цепочке выполнения.

Обтекание ошибок может предоставить дополнительный контекст для ошибки аналогично трассировке стека. Обтекание также сохраняет исходную ошибку, что означает, что errors.Is и errors.As продолжают работать, независимо от того, сколько раз ошибка была перенесена. Мы также можем использовать errors.Unwrap it для возврата предыдущей ошибки в цепочке.

Заключение

В итоге,

  • Ошибки в Go — это значения https://go.dev/blog/errors-are-values
  • Ошибки помогают улучшить отладку основной причины сбоя программы.
  • Ошибки переноса предоставляют контекст для трассировки через последовательность вызовов функций.

Дополнительное чтение