← назад к разделу

В большинстве языков ошибки — это исключения: что-то ломается, выполнение «выпрыгивает» из функции, и где-то выше его ловит try/catch. В Go всё устроено иначе. Здесь ошибка — это обычное значение, которое функция возвращает наравне с результатом. Разберём, почему так сделано и как с этим жить.

Зачем ошибки как значения

Главная идея Go: ошибка — это часть результата функции, а не отдельный поток управления. Функция, которая может не справиться, возвращает два значения — результат и error:

func ReadConfig(path string) (Config, error) {
    // ...
}

Вызывающий код обязан посмотреть на второе значение. Ошибку нельзя «случайно не заметить», как незаловленное исключение: она лежит прямо перед тобой как переменная. Минус — проверок становится много. Плюс — поток выполнения линеен и предсказуем: ты всегда видишь, где именно что-то может пойти не так.

Короткая формула: в Go не «бросают» ошибку, её возвращают и тут же проверяют.

Что такое error

error — это встроенный interface с единственным методом:

type error interface {
    Error() string
}

Любой тип, у которого есть метод Error() string, является ошибкой. Это значит, что ошибка — не магия, а обычное значение какого-то типа. Если ошибки нет, функция возвращает nil на месте error.

value, err := strconv.Atoi("42") // превращает строку в число
if err != nil {
    // сюда не зайдём: "42" корректно
}
// value == 42

Подробнее про интерфейсы — в отдельной статье (ссылка ниже); здесь достаточно знать, что error — это контракт «я умею рассказать о себе строкой».

Идиома if err != nil

Базовый приём, который ты будешь писать постоянно:

f, err := os.Open("data.txt")
if err != nil {
    return err // не справились — сообщаем выше
}
// дальше работаем с f, зная, что ошибки нет

Несколько правил, которые делают этот код идиоматичным:

  • Проверяй ошибку сразу после вызова, до использования результата.
  • Если обработать ошибку здесь нельзя — возвращай её выше (return err), пусть разбирается тот, кто знает контекст.
  • При ошибке остальные возвращаемые значения обычно бессмысленны — не используй их.
// типичная цепочка: каждый шаг может сорваться
data, err := os.ReadFile(path)
if err != nil {
    return err
}
cfg, err := parse(data)
if err != nil {
    return err
}
return validate(cfg)

Да, проверок много. Это осознанный выбор языка: явность важнее краткости.

Как создавать ошибки

Два основных способа сделать ошибку.

errors.New — простая ошибка с фиксированным текстом:

import "errors"

var ErrNotFound = errors.New("запись не найдена")

func find(id int) (Item, error) {
    // ...
    return Item{}, ErrNotFound
}

Ошибку часто кладут в переменную с префиксом Err (так называемая sentinel-ошибка) — чтобы выше её можно было сравнить и принять решение.

fmt.Errorf — когда в текст нужно подставить данные:

import "fmt"

func find(id int) (Item, error) {
    // ...
    return Item{}, fmt.Errorf("запись id=%d не найдена", id)
}

fmt.Errorf форматирует строку так же, как fmt.Printf, но возвращает error.

Обёртывание и errors.Is / errors.As

Когда ошибка поднимается через несколько слоёв, голый текст вроде "запись не найдена" не помогает: непонятно, где и при чём это случилось. Решение — обёртывание (wrapping): добавить контекст, но сохранить исходную ошибку внутри.

Для этого в fmt.Errorf используется глагол %w (от wrap):

func loadUser(id int) (User, error) {
    u, err := repo.find(id)
    if err != nil {
        // добавляем контекст, но не теряем исходную ошибку
        return User{}, fmt.Errorf("loadUser id=%d: %w", id, err)
    }
    return u, nil
}

Теперь текст ошибки накапливает «маршрут» (loadUser id=7: запись не найдена), а внутри по-прежнему лежит оригинал. Достать его помогают две функции.

errors.Is — проверить, что где-то в цепочке обёрток есть конкретная sentinel-ошибка:

_, err := loadUser(7)
if errors.Is(err, ErrNotFound) {
    // да, в глубине это именно "не найдено" — отвечаем соответствующе
}

Сравнивать через == здесь нельзя: обёрнутая ошибка — уже другое значение. errors.Is разворачивает слои за тебя.

errors.As — когда нужна не просто проверка, а доступ к полям ошибки определённого типа:

type ValidationError struct {
    Field string
}

func (e *ValidationError) Error() string {
    return "некорректное поле: " + e.Field
}

var vErr *ValidationError
if errors.As(err, &vErr) {
    // нашли ValidationError в цепочке — можно прочитать vErr.Field
    fmt.Println("проблемное поле:", vErr.Field)
}

Короткая формула: оборачивай через %w, проверяй значение через errors.Is, извлекай тип через errors.As.

diagram

panic и recover — редкий случай

В Go есть и механизм, похожий на исключения: panic разворачивает стек и аварийно завершает программу, а recover позволяет его перехватить. Но это не замена обычным ошибкам.

panic уместен только для ситуаций «программа в принципе не может продолжать»: нарушен внутренний инвариант, некорректно собрана программа, обращение за границу slice. Для ожидаемых сбоев (файл не найден, неверный ввод) — всегда возвращай error.

func recovered() {
    defer func() {
        if r := recover(); r != nil {
            // перехватили панику, не дали процессу упасть
            fmt.Println("восстановились после:", r)
        }
    }()
    panic("что-то пошло совсем не так")
}

recover работает только внутри defer-функции. Типичное применение — на границе, чтобы одна сбойная задача не уронила весь сервис. Правило по умолчанию: ошибки возвращай, panic не используй.

Ошибки на уровне HTTP и backend

Этот раздел — про язык. Как ошибки превращаются в HTTP-ответы (коды статусов, единый формат тела, логирование на границе обработчика) — отдельная тема backend-уровня. Об этом — в статье Ошибки и HTTP в Go из раздела про серверную разработку.

Коротко

  • Ошибка в Go — это значение типа error (интерфейс с методом Error() string), а не исключение.
  • Функция возвращает error рядом с результатом; вызывающий проверяет его идиомой if err != nil.
  • Создавай ошибки через errors.New (фиксированный текст) или fmt.Errorf (с данными).
  • Оборачивай ошибку через %w, чтобы добавить контекст и сохранить оригинал внутри.
  • errors.Is ищет в цепочке конкретную sentinel-ошибку, errors.As — извлекает ошибку нужного типа.
  • panic/recover — только для неустранимых сбоев; для ожидаемых ошибок всегда возвращай error.

Что почитать дальше

  • Интерфейсы в Go — почему error это interface и как устроены контракты типов.
  • Горутины и каналы — как передавать ошибки из параллельного кода.
  • Синтаксис и типы — основа, если множественный возврат и := ещё в новинку.