Перейти до змісту

Помилки

У Go немає виключень. Коли функція може зазнати невдачі, вона повертає додаткове значення типу error, і виклик явно перевіряє його.

n, err := strconv.Atoi("forty-two")
if err != nil {
    fmt.Println("parse failed:", err)
    return
}
fmt.Println(n)

Цей патерн — перевірити err, відреагувати, повернутися — є найпоширенішим п'ятирядковим блоком у будь-якій Go-програмі.

З досвіду Python: Python використовує виключення: невдача перериває потік виконання та розкручує стек, поки щось її не перехопить. Go робить невдачу звичайним значенням повернення. Ментальний зсув реальний: кожна операція, що може зазнати невдачі, видима на місці виклику, і ви обробляєте її (або явно передаєте далі) прямо там.

Тип error

error — це вбудований інтерфейс з одним методом. (Інтерфейс, поки що, — це іменований набір сигнатур методів, який будь-який тип може задовольнити, реалізувавши їх — детальний розгляд буде в окремій статті.)

type error interface {
    Error() string
}

Все, що має метод Error() string, задовольняє його. Поки що не потрібно знати, як визначити власний тип помилки — це розглядається після методів та інтерфейсів.

Нульове значення errornil, що означає «помилки немає». Саме тому if err != nil { ... } є стандартною перевіркою.

Створення помилки: errors.New

errors.New повертає нову помилку, що містить лише повідомлення.

import "errors"

err := errors.New("something went wrong")
fmt.Println(err)                    // something went wrong
fmt.Println(err.Error())            // something went wrong

Кожен виклик повертає окреме значення — дві помилки з однаковим текстом не є рівними за ==:

e1 := errors.New("oops")
e2 := errors.New("oops")
fmt.Println(e1 == e2)               // false

Якщо потрібна порівнювана помилка рівня пакету для подальших перевірок, оголосіть її один раз і використовуйте повторно:

var ErrNotFound = errors.New("not found")

func lookup(id int) (string, error) {
    if id == 0 {
        return "", ErrNotFound
    }
    return "found", nil
}

if _, err := lookup(0); err == ErrNotFound {
    fmt.Println("nothing matched")
}

Такі помилки рівня пакету називають сигнальними помилками (sentinel errors). Угода: іменувати їх ErrXxx.

Обгортання помилок із контекстом: fmt.Errorf + %w

Коли помилка проходить через кілька шарів, кожен шар зазвичай хоче додати контекст. Дієслово %w у fmt.Errorf будує обгорнуту помилку, що зберігає оригінальну під новим повідомленням.

func loadConfig(path string) error {
    data, err := os.ReadFile(path)
    if err != nil {
        return fmt.Errorf("loadConfig %q: %w", path, err)
    }
    _ = data
    return nil
}

Отриманий рядок читається природно:

loadConfig "missing.toml": open missing.toml: no such file or directory

Але важливіше те, що підлягаюча помилка залишається доступною для перевірки через errors.Is та errors.As.

Використовуйте %w рівно один раз на виклик fmt.Errorf. Щоб вбудувати повідомлення про помилку без обгортання (рідко), використовуйте %s або %v.

Перевірка обгорнутих помилок: errors.Is та errors.As

errors.Is(err, target) bool

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

_, err := os.Open("missing.txt")
if errors.Is(err, fs.ErrNotExist) {
    fmt.Println("file does not exist")
}

Перевірка витримує будь-яку кількість обгорток fmt.Errorf("...: %w", err). Надавайте перевагу errors.Is(err, ErrFoo) замість err == ErrFoo — воно працює так само, коли нічого не обгорнуто, і продовжує працювати після додавання обгорток.

errors.As(err, &target) bool

Обходить ланцюжок обгорток у пошуку помилки конкретного типу і при успіху копіює її в target. Використовуйте це, коли потрібно читати поля підлягаючої помилки.

_, err := os.Open("missing.txt")
var pathErr *fs.PathError
if errors.As(err, &pathErr) {
    fmt.Println("failed at path:", pathErr.Path)
    fmt.Println("operation was:", pathErr.Op)
}

Завжди передавайте вказівник на цільову змінну.

Об'єднання кількох помилок: errors.Join

Іноді функція виконує кілька незалежних кроків і ви хочете повідомити про кожну невдачу, а не лише про першу. errors.Join об'єднує кілька помилок в одну; errors.Is/As потім обходять усі з них.

err1 := errors.New("disk full")
err2 := errors.New("network down")

both := errors.Join(err1, err2)
fmt.Println(both)
// disk full
// network down

fmt.Println(errors.Is(both, err1))   // true
fmt.Println(errors.Is(both, err2))   // true

Аргументи nil пропускаються. Якщо всі аргументи nil, результат — nil.

Головні правила

  1. Перевіряйте кожну помилку. Відкинута err — майже завжди баг. Використовуйте _ лише тоді, коли є свідома причина та коментар.
  2. Обгортайте з контекстом. Коли ви повертаєте помилку, яку не ви породили, додайте те, що ви знаєте (який файл, який користувач, яка операція).
  3. Надавайте перевагу errors.Is над ==. Воно працює еквівалентно в необгорнутому випадку і захищає вас, коли обгортання буде додано пізніше.
  4. Не ховайте перевірки помилок у структурах, що маскують їх. Ніяких try/except-подібних посередників. Блок if err != nil { return err } є ідіомою — повторення доречне; воно робить шляхи невдачі очевидними.

Чим error не є

  • Не виключенням. Неявного поширення немає; ви повертаєте його.
  • Не sum-типом / Result<T, E>. Два значення повернення є незалежними — за угодою, якщо err != nil, ігноруйте інше значення; якщо err == nil, довіряйте йому.
  • Не правильним інструментом для «програма досягла неможливого стану». Для цього є panic (12-panic-and-recover.md).

Джерела