Go: с нуля до своего блога Урок 14 из 50
Ошибки в Go: не исключение, а обычное значение
Четырнадцатый урок курса по Go. Ошибка здесь не прерывает программу, а возвращается вторым значением, и проверяют её первой. error как интерфейс с одним методом, errors.New и fmt.Errorf, обёртка через %w и errors.Is, своя ошибка как значение — и почему panic для этого не нужен.
Зачем это нужно
Читатель открыл /read/kokek, а такой статьи нет. Это не поломка программы — это обычный ход событий, на который надо ответить кодом 404.
В Go для этого нет try и catch. Функция просто возвращает два значения: результат и ошибку. Вы смотрите на вторую и решаете, что делать.
Образ. Не сирена, а квитанция. Сирена срывает с места всё здание. Квитанция — бумажка, которую вам отдали в руки: прочитайте и решите, идти дальше или разворачиваться.
Сразу целиком
Новая папка, go mod init sabaq13, main.go:
package main
import (
"errors"
"fmt"
)
type Article struct {
Title string
Words int
}
var ErrNotFound = errors.New("статья не найдена")
type Store struct {
items map[string]Article
}
func (s *Store) Get(slug string) (Article, error) {
a, ok := s.items[slug]
if !ok {
return Article{}, fmt.Errorf("get %q: %w", slug, ErrNotFound)
}
return a, nil
}
func show(s *Store, slug string) {
a, err := s.Get(slug)
if err != nil {
if errors.Is(err, ErrNotFound) {
fmt.Println("404:", err)
return
}
fmt.Println("500:", err)
return
}
fmt.Println(a.Title, "·", a.Words, "слов")
}
func main() {
s := &Store{items: map[string]Article{
"dala": {Title: "О степи", Words: 400},
}}
show(s, "dala")
show(s, "kokek")
_, err := s.Get("kokek")
fmt.Println(errors.Is(err, ErrNotFound))
fmt.Println(errors.Unwrap(err) == ErrNotFound)
}
Прежде чем запускать — не заглядывая ниже, скажите вслух, что она напечатает. Потом запустите и сверьте.
О степи · 400 слов
404: get "kokek": статья не найдена
true
true
Разбор
error — интерфейс с одним методом
Вчерашний урок сделал сегодняшний почти очевидным. error — не особая конструкция языка, а обычный интерфейс из одного метода:
type error interface {
Error() string
}
Всё, что умеет Error() string, годится в ошибки. Никакой иерархии классов, никаких «типов исключений» — тип, у которого есть один метод.
Функция возвращает два значения
func (s *Store) Get(slug string) (Article, error)
Слева результат, справа ошибка — этот порядок в Go соблюдают всегда. Если ошибки нет, вторым приходит nil, а первое значение осмысленно. Если ошибка есть, первое значение обычно нулевое, и полагаться на него нельзя.
Отсюда правило чтения чужого кода: увидели вызов с двумя результатами — ищите глазами проверку второго.
if err != nil — и почему это не уродство
a, err := s.Get(slug)
if err != nil {
return
}
Эти три строки в Go встречаются чаще любых других, и их принято ругать. Возразим по существу: они делают путь ошибки видимым. В языке с исключениями по коду не понять, какая строка может прервать функцию, — здесь понятно с одного взгляда.
Плата за это — многословность. Выгода — вы никогда не удивитесь, откуда вылетело.
errors.New и fmt.Errorf
errors.New("текст") создаёт ошибку с сообщением. fmt.Errorf делает то же, но умеет подставлять значения:
fmt.Errorf("get %q: %w", slug, ErrNotFound)
Здесь два глагола. %q подставляет адрес статьи в кавычках. %w — особый: он заворачивает другую ошибку внутрь новой, сохраняя её целой.
Поэтому напечаталось get "kokek": статья не найдена — сначала где, потом что.
Образ. Конверт в конверте. Каждый, через чьи руки прошло письмо, кладёт его в свой конверт и подписывает: «получено оттуда-то». Вскрыв все, вы видите и путь, и исходное письмо.
Правило простое: сообщение об ошибке пишут строчной буквой и без точки, потому что оно почти всегда окажется в середине чужой строки.
Своя ошибка как значение и errors.Is
var ErrNotFound = errors.New("статья не найдена")
Ошибку объявляют переменной уровня пакета и называют с приставки Err. Тогда её можно сравнивать, а не разбирать текст:
if errors.Is(err, ErrNotFound) {
errors.Is разворачивает все конверты и проверяет, нет ли внутри именно этой ошибки. Поэтому наша обёрнутая %w-ошибка узналась, хотя текст у неё уже другой.
Никогда не сравнивайте ошибки по тексту: err.Error() == "статья не найдена" сломается от первой же правки формулировки, и молча.
Что делать с ошибкой
Ровно три достойных варианта.
- Обработать. Знаете, что делать, — сделайте: 404 вместо статьи, значение по умолчанию, повтор.
- Обернуть и вернуть выше. Не знаете — добавьте свой конверт с
%wи отдайте наверх, где знают. - Записать в журнал. Один раз и на самом верху, а не в каждом слое.
И один недостойный: проглотить. a, _ := s.Get(slug) — законный Go, компилятор промолчит, и программа пойдёт дальше с пустой статьёй. Подчёркивание на месте ошибки допустимо только там, где вы можете вслух объяснить, почему она не важна.
panic — не для ошибок
panic останавливает программу. Он нужен для ситуаций, из которых нет разумного выхода: не собралась конфигурация, не подключился обязательный шаблон при старте.
Ненайденная статья — не такая ситуация. Их будут тысячи в день.
Образ. Пожарная тревога. Она есть в каждом здании, и это правильно. Но нажимать её потому, что посетитель не нашёл нужный кабинет, никто не станет.
Карта урока
Скажите своими словами
Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.
- Что такое
errorс точки зрения языка и почему после урока про интерфейсы это уже не удивляет? - Чем
%wотличается от%vвfmt.Errorfи что от этого зависит? - Почему
err.Error() == "статья не найдена"— плохой способ узнать ошибку, аerrors.Is— хороший?
Задание
Обязательное. Добавьте типу Store метод Add(slug string, a Article) error. Он должен вернуть ошибку ErrEmptyTitle, если заголовок пустой, и nil, если статья принята. Напишите тест: добавьте нормальную статью и убедитесь, что ошибки нет; добавьте статью с пустым заголовком и проверьте errors.Is(err, ErrEmptyTitle); убедитесь, что вторая в хранилище не попала.
По желанию.
- Оберните
ErrEmptyTitleчерез%w, добавив адрес статьи, и проверьте, чтоerrors.Isпо-прежнему узнаёт её. - Напечатайте
errors.Unwrap(err)и посмотрите, что внутри конверта. - Замените в
showпроверкуerrors.Isна сравнение текстов, а потом поменяйте формулировкуErrNotFoundи посмотрите, что сломается.
Куда это встанет в блоге
Так устроен и сайт, который вы читаете. Хранилище возвращает ErrNotFound, обработчик вызывает errors.Is и отвечает 404, а всё остальное превращается в 500 и одну строчку в журнале. Никаких исключений, никаких перехватов на десять уровней вверх — второе значение и одна проверка.
Ответы
Показать ответы
error— обычный интерфейс с единственным методомError() string. После прошлого урока это не удивляет: любой тип с этим методом годится в ошибки, объявлять ничего не надо.%vвставляет текст ошибки, и связь с исходной теряется.%wзаворачивает саму ошибку внутрь новой, поэтомуerrors.Isиerrors.Unwrapпотом до неё доберутся. От этого зависит, сможет ли вызывающий отличить «не найдено» от «всё сломалось».- Текст — это сообщение для человека, и его меняют, не задумываясь: перевели на другой язык, добавили подробность — и сравнение перестало работать, причём молча.
errors.Isсравнивает значения, а не буквы, и переживает любую правку формулировки.
Источники
Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом
Комментарии (0)
Войдите, чтобы оставить комментарий →
Пока нет комментариев. Будьте первым.