Помилки¶
У 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 — це вбудований інтерфейс з одним методом. (Інтерфейс, поки що, — це іменований набір сигнатур методів, який будь-який тип може задовольнити, реалізувавши їх — детальний розгляд буде в окремій статті.)
Все, що має метод Error() string, задовольняє його. Поки що не потрібно
знати, як визначити власний тип помилки — це розглядається після методів
та інтерфейсів.
Нульове значення error — nil, що означає «помилки немає». Саме тому
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
Кожен виклик повертає окреме значення — дві помилки з однаковим
текстом не є рівними за ==:
Якщо потрібна порівнювана помилка рівня пакету для подальших перевірок, оголосіть її один раз і використовуйте повторно:
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
}
Отриманий рядок читається природно:
Але важливіше те, що підлягаюча помилка залишається доступною для
перевірки через 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.
Головні правила¶
- Перевіряйте кожну помилку. Відкинута
err— майже завжди баг. Використовуйте_лише тоді, коли є свідома причина та коментар. - Обгортайте з контекстом. Коли ви повертаєте помилку, яку не ви породили, додайте те, що ви знаєте (який файл, який користувач, яка операція).
- Надавайте перевагу
errors.Isнад==. Воно працює еквівалентно в необгорнутому випадку і захищає вас, коли обгортання буде додано пізніше. - Не ховайте перевірки помилок у структурах, що маскують їх. Ніяких
try/except-подібних посередників. Блокif err != nil { return err }є ідіомою — повторення доречне; воно робить шляхи невдачі очевидними.
Чим error не є¶
- Не виключенням. Неявного поширення немає; ви повертаєте його.
- Не sum-типом /
Result<T, E>. Два значення повернення є незалежними — за угодою, якщоerr != nil, ігноруйте інше значення; якщоerr == nil, довіряйте йому. - Не правильним інструментом для «програма досягла неможливого стану».
Для цього є
panic(12-panic-and-recover.md).