Перейти к содержимому
Теория · Ошибки · урок 1 из 11

Ошибка — это значение

Чему вы научитесь

Чему вы научитесь:

  • объяснять, почему ошибка в Go — обычное значение, и читать сигнатуры вида (int, error);
  • создавать ошибки через errors.New и fmt.Errorf;
  • проверять ошибку сразу после вызова через if err != nil;
  • оборачивать ошибку через %w и находить её через errors.Is.
Курьер двумя руками передаёт получателю коробку, а сверху лежит накладная с пометкой err

Представьте, что вы заказали посылку. Курьер отдаёт коробку, а вместе с ней накладную. Обычно накладная чистая, но иногда в ней пометка: «товар повреждён» или «не тот размер». Разумно сначала прочитать накладную и только потом открывать коробку.

Функции в Go, которые могут не справиться с работой, устроены так же. Не каждую строку можно превратить в число или в bool. Функция strconv.ParseBool из пакета strconv понимает "true" и "false", а слово "yes" разобрать не может. О неудаче она сообщает вторым результатом — это и есть накладная:

GO
package main

import (
    "fmt"
    "strconv"
)

func main() {
    b, err := strconv.ParseBool("yes")
    fmt.Println(b, err) // false strconv.ParseBool: parsing "yes": invalid syntax
    b, err = strconv.ParseBool("true")
    fmt.Println(b, err) // true <nil>
}

Давайте разберём пример по строкам:

  1. Первый вызов получает "yes" и возвращает false вместе с ошибкой.
  2. Первый Println печатает оба результата. Текст ошибки называет функцию, строку и причину: invalid syntax.
  3. Второй вызов разбирает "true" и возвращает true.
  4. Второй Println печатает true <nil>: во втором результате <nil> — чистая накладная.

Программа не останавливается и не падает. Ошибка приходит в переменную err, как обычное значение, и программа решает, что с ней делать. Когда разбор удался, в err лежит nil.

Сигнатура с ошибкой

Уже по сигнатуре видно, что функция может не справиться. Сигнатура — это строка с именем функции, её параметрами и типами результатов. У strconv.ParseBool она такая: func ParseBool(str string) (bool, error). Разберём её части:

  • ParseBool — имя функции;
  • (str string) — параметр: строка, которую нужно разобрать;
  • (bool, error) — два результата: само значение и ошибка.

Второй результат типа error предупреждает вызывающий код ещё до первого запуска. Взять только первый результат нельзя: вызов с одной переменной слева не скомпилируется.

Интерфейс error

Тип error встроен в язык, как int. О нём нужно знать три вещи:

  • это интерфейс с одним методом: Error() string;
  • метод возвращает текст ошибки, и fmt.Println печатает именно его;
  • значением типа error может быть любой тип с таким методом.

Вот свой тип ошибки и функция, которая его возвращает:

GO
type tooOld int

func (a tooOld) Error() string {
    return fmt.Sprintf("age %d is too big", int(a))
}

func checkAge(n int) (int, error) {
    if n > 150 {
        return 0, tooOld(n)
    }
    return n, nil
}

Функция checkAge возвращает значение tooOld(n) там, где ждут error. Компилятор это принимает: у tooOld есть метод Error() string, поэтому тип реализует интерфейс. Так устроены и ошибки стандартной библиотеки: у каждой есть тип с методом Error().

nil — значит успех

Нулевое значение интерфейса — nil. Для ошибки оно значит «всё прошло успешно», поэтому checkAge возвращает n, nil для допустимого возраста:

GO
age, err := checkAge(30)
fmt.Println(age, err) // 30 <nil>
age, err = checkAge(200)
fmt.Println(age, err) // 0 age 200 is too big

Разберём оба вызова:

  1. checkAge(30) проходит проверку и печатает 30 <nil>.
  2. checkAge(200) возвращает 0 и ошибку, а fmt.Println печатает её текст: age 200 is too big.

Два правила для результатов

У функций, которые возвращают ошибку, два правила:

  1. Ошибка стоит последним результатом.
  2. Если она не nil, на остальные результаты не полагаются: обычно в них нулевые значения своих типов.

Поэтому при отказе checkAge возвращает 0, а не сам возраст.

Здесь картинка с посылкой перестаёт работать. В жизни из помятой коробки иногда достают что-то целое. В Go при ошибке содержимое коробки не трогают: накладная говорит, что верить ему нельзя.

Сравнение с nil

Значение err сравнивают с nil так же, как указатель:

  • err == nil — успех;
  • err != nil — неудача.

Текст ошибки для этого читать не нужно. При успехе возвращают именно nil.

Это классический вопрос на собеседовании: если вернуть как error пустой указатель своего типа ошибки, вызывающий код получит err != nil. Как вы уже знаете из темы об интерфейсах, интерфейс с nil-указателем внутри не равен nil.

Частая ошибка

При ошибке вместо числа вернули nil:

GO
func checkAge(n int) (int, error) {
    if n > 150 {
        return nil, tooOld(n) // ✗
    }
    return n, nil
}
TEXT
cannot use nil as int value in return statement

Значение nil бывает у указателя, среза, map или интерфейса, но не у числа. Для результата типа int верните его нулевое значение: return 0, tooOld(n).

Главное

  • Ошибка в Go — обычное значение типа error; её возвращают последним результатом.
  • error — интерфейс с одним методом Error() string, и его реализует любой тип с таким методом.
  • err == nil значит успех; при err != nil на остальные результаты не полагаются.
  • При успехе возвращают nil, а при ошибке — нулевые значения остальных результатов, например 0.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом