Треки
/
Go
Go
/
Салабус
/
Помилки
По

Помилки у Go

2 вправи

Про концепцію Помилки

Інтерфейс error

У Go обробка помилок не відбувається через винятки. Натомість помилки - це звичайні значення типів, які реалізують вбудований інтерфейс error. Інтерфейс error дуже мінімальний. Він містить лише один метод Error(), який повертає повідомлення про помилку як рядок тексту (англ. string).

type error interface {
  Error() string
}

Щоразу, коли ми визначаємо функцію, під час виконання якої може статися помилка, що має дійти до того, хто викликав функцію, потрібно додати error до переліку типів, які повертає функція. Якщо функція повертає кілька значень, за домовленістю error завжди стоїть останнім.

func DoSomething() (int, error) {
  // ...
}

Створення і повернення простої помилки

Не обовʼязково щоразу реалізовувати інтерфейс error самостійно. Щоб створити просту помилку, можна скористатися функцією errors.New() зі стандартного пакета errors. Єдине, що потрібно передати, - це повідомлення про помилку як рядок тексту, а errors.New() подбає про створення значення, яке містить це повідомлення і реалізує інтерфейс error.

Якщо функція повертає помилку, доброю практикою є повертати нульове значення для всіх інших значень, які повертаються:

func DoSomething() (SomeStruct, int, error) {
  // ...
  return SomeStruct{}, 0, errors.New("failed to calculate result")
}
Caution

Не варто припускати, що всі функції повертають нульові значення для інших значень, якщо сталася помилка. Найкраще вважати, що користуватися жодним з інших повернених значень небезпечно, якщо функція повернула помилку.

Наприклад, уявімо функцію, яка намагалася прочитати дані з файлу і повертає наполовину заповнений зріз байтів та помилку. Якщо ми повторно використаємо цей повернений зріз байтів у коді, припускаючи, що він завжди порожній, бо сталася помилка, можна натрапити на неочевидні bug.

Винятки - це лише випадки, коли документація функції чітко зазначає, що інші повернені значення мають сенс у разі помилки. Погляньмо на [документацію Write для буфера][buffer-write] як на приклад.

Подбаймо про те, щоб повідомлення про помилку було якомога конкретнішим, адже помилки за замовчуванням не містять стеків викликів. За домовленістю повідомлення про помилку має починатися з малої літери і не закінчуватися крапкою.

Якщо потрібно використати таку помилку в кількох місцях (або зробити помилку доступною для користувача нашого пакета), варто оголосити для неї змінну замість того, щоб викликати errors.New прямо в коді. За домовленістю назва змінної має починатися з Err або err (залежно від того, експортована вона чи ні). Такі змінні для помилок часто називають сторожовими помилками.

import "errors"

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

func DoSomething() error {
  // ...
  return ErrNotFound
}

Повернімо nil як помилку, щоб показати, що під час виконання функції помилок не було:

func Foo() (int, error) {
  return 10, nil
}

Додавання змінних до повідомлення про помилку

Якщо потрібно побудувати складніше повідомлення про помилку з кількох змінних, можна скористатися форматуванням рядків. Для цього в Go є спеціальна функція fmt.Errorf. Вона будує рядок тексту повідомлення про помилку і повертає помилку.

input := 123
action := "UPDATE"

err := fmt.Errorf("invalid input %d for action %s", input, action)
err.Error()
// => "invalid input 123 for action UPDATE"

Перевірка помилок

Якщо ми викликаємо функцію, яка повертає помилку, зазвичай зберігаємо значення помилки у змінній з назвою err. Перш ніж скористатися справжнім результатом функції, потрібно перевірити, що помилки не було. Ми можемо порівняти помилку з nil за допомогою == і !=, і знаємо, що помилка сталася, коли err не дорівнює nil.

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

// Do this:
func myFunc() error {
  file, err := os.Open("./users.csv")
  if err != nil {
    return err
  }
  // do something with file
}

// Not this:
func myFunc() error {
  file, err := os.Open("./users.csv")
  if err != nil {
    // handle err
  } else {
    // do something with file
  }
}

// Also not this:
func myFunc() error {
  file, err := os.Open("./users.csv")
  if err == nil {
    // do something with file
  }
  // handle err
}

Здебільшого помилку повертають угору по стеку функцій, як у першому прикладі вище. Інший спосіб обробити помилку - записати її в журнал і продовжити з іншою операцією. Добра практика - або повернути помилку, або записати її в журнал, і ніколи не робити і того, і іншого.

Оскільки більшість функцій у Go повертає помилку як одне зі значень, ми будемо бачити й використовувати конструкцію if err != nil усюди в Go-коді.

Власні типи помилок

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

Зазвичай для створення власного типу помилки використовують структуру. За домовленістю назви власних типів помилок мають закінчуватися на Error. Крім того, метод Error() string найкраще визначати з отримувачем-вказівником; про причини цього йдеться в цьому коментарі на Stack Overflow. Зауважмо, що тоді потрібно повертати вказівник на власну помилку, інакше вона не вважатиметься error, бо значення без вказівника не надає методу Error() string.

type MyCustomError struct {
  message string
  details string
}

func (e *MyCustomError) Error() string {
  return fmt.Sprintf("%s, details: %s", e.message, e.details)
}

func someFunction() error {
  // ...
  return &MyCustomError{
    message: "...",
    details: "...",
  }
}
Редагувати через GitHub Посилання відкривається в новому вікні або вкладці

Вивчити концепцію Помилки

Практика заблокована

Розблокуйте ще 4 вправи, щоб практикувати концепцію Помилки