Shanraq.org Shanraq.org
Ошибки в Go: не исключение, а обычное значение
IT

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() == "статья не найдена" сломается от первой же правки формулировки, и молча.

Что делать с ошибкой

Ровно три достойных варианта.

  1. Обработать. Знаете, что делать, — сделайте: 404 вместо статьи, значение по умолчанию, повтор.
  2. Обернуть и вернуть выше. Не знаете — добавьте свой конверт с %w и отдайте наверх, где знают.
  3. Записать в журнал. Один раз и на самом верху, а не в каждом слое.

И один недостойный: проглотить. a, _ := s.Get(slug) — законный Go, компилятор промолчит, и программа пойдёт дальше с пустой статьёй. Подчёркивание на месте ошибки допустимо только там, где вы можете вслух объяснить, почему она не важна.

panic — не для ошибок

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

Ненайденная статья — не такая ситуация. Их будут тысячи в день.

Образ. Пожарная тревога. Она есть в каждом здании, и это правильно. Но нажимать её потому, что посетитель не нашёл нужный кабинет, никто не станет.

Карта урока

Карта урока: у функции два выхода — значение и ошибка

Скажите своими словами

Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.

  1. Что такое error с точки зрения языка и почему после урока про интерфейсы это уже не удивляет?
  2. Чем %w отличается от %v в fmt.Errorf и что от этого зависит?
  3. Почему 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 и одну строчку в журнале. Никаких исключений, никаких перехватов на десять уровней вверх — второе значение и одна проверка.

Ответы

Показать ответы
  1. error — обычный интерфейс с единственным методом Error() string. После прошлого урока это не удивляет: любой тип с этим методом годится в ошибки, объявлять ничего не надо.
  2. %v вставляет текст ошибки, и связь с исходной теряется. %w заворачивает саму ошибку внутрь новой, поэтому errors.Is и errors.Unwrap потом до неё доберутся. От этого зависит, сможет ли вызывающий отличить «не найдено» от «всё сломалось».
  3. Текст — это сообщение для человека, и его меняют, не задумываясь: перевели на другой язык, добавили подробность — и сравнение перестало работать, причём молча. errors.Is сравнивает значения, а не буквы, и переживает любую правку формулировки.

Источники

Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом

Проверить задание

Сначала решите и запустите в VS Code — редактор покажет ошибку на месте. Готовое решение вставьте сюда. Проверяет модель: она укажет на ошибку, но не даст готовый ответ.

Чтобы проверить, нужно войти. Войти

Комментарии (0)

Пока нет комментариев. Будьте первым.