В большинстве языков ошибки — это исключения: что-то ломается, выполнение «выпрыгивает» из функции, и где-то выше его ловит 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.
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 и как устроены контракты типов. - Горутины и каналы — как передавать ошибки из параллельного кода.
- Синтаксис и типы — основа, если множественный возврат и
:=ещё в новинку.