Menu

Обработка ошибок в Golang: if err != nil, обёртки и проверки

В Go ошибки это обычные значения, которые возвращают функции. Интерфейс error, if err != nil, errors.New и fmt.Errorf, возврат ошибок с контекстом, их проверка через errors.Is и errors.As и правило обрабатывать каждую ошибку один раз.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

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

Функция в Go, которая может завершиться неудачей, возвращает error последним результатом. Вызывающий код сразу его проверяет.

Вывод:

parsed: 42
could not parse: strconv.Atoi: parsing "forty-two": invalid syntax

Вот и весь механизм. Нет исключений, нет try и catch, нет скрытого потока управления: ошибка попадает только туда, куда её передаёт ваш код. Цена это заметное повторение if err != nil. Выгода в том, что каждая точка отказа видна в коде, и в каждой вы сами решаете, что делать.

Тип error

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

type error interface {
	Error() string
}

Любой тип с методом Error() string является ошибкой. Ошибка, равная nil, означает успех. Печать ошибки через fmt.Println(err) или %v вызывает Error().

Создание ошибок

Большинство случаев покрывают две функции.

errors.New создаёт ошибку с фиксированным текстом. fmt.Errorf форматирует её теми же глаголами, что и Printf. По соглашению строки ошибок начинаются со строчной буквы и не заканчиваются знаками препинания, потому что обычно встраиваются в более длинные сообщения: load config: open app.yaml: no such file or directory.

Шаблон if err != nil

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

func loadUser(id int) (*User, error) {
	row, err := db.Query(id)
	if err != nil {
		return nil, err
	}
	u, err := parseUser(row)
	if err != nil {
		return nil, err
	}
	if err := u.Validate(); err != nil {
		return nil, err
	}
	return u, nil
}

Два соглашения, на которые стоит обратить внимание:

  • При ошибке возвращайте нулевое значение для остальных результатов (nil, 0, ""). Вызывающий код не должен их использовать, когда err != nil.
  • if err := f(); err != nil ограничивает область видимости err этим if, когда функция возвращает только ошибку. Внешняя область видимости остаётся чистой.

Не пишите else после возврата ошибки. if err != nil { return err } else { ... } просто сдвигает успешный путь вправо без всякой причины.

Контекст при возврате ошибки

Ошибка, переданная наверх без изменений, теряет историю своего происхождения. open config.yaml: no such file or directory не говорит, на каком шаге запуска случился сбой. Добавьте контекст через fmt.Errorf и глагол %w:

Вывод:

start server: read config: open /etc/myapp/config.yaml: no such file or directory
true

Каждый уровень добавляет то, что он делал, и итоговое сообщение читается как след от верхушки вызовов до причины. Хороший контекст называет операцию и входные данные: parse line 12, fetch user 42. Не добавляйте «error» или «failed» на каждом уровне: сообщение и так об ошибке.

%w оборачивает: сохраняет исходную ошибку внутри новой, так что errors.Is и errors.As по-прежнему могут её найти. %v только копирует текст. Используйте %v, когда намеренно хотите скрыть деталь реализации от вызывающего кода, например чтобы он не начал зависеть от типа ошибки драйвера базы данных.

Проверка конкретных ошибок: errors.Is и errors.As

Иногда вызывающему коду нужно отреагировать на определённый вид сбоя: отсутствующий файл означает «использовать значения по умолчанию», таймаут означает «повторить». На это отвечают две функции, и обе просматривают все слои обёрток.

Практические правила:

  • Сравнивайте с заранее определёнными значениями ошибок (маркерами вроде io.EOF, os.ErrNotExist, sql.ErrNoRows) через errors.Is, а не ==. == перестаёт работать, как только ошибку обернули.
  • Извлекайте типизированную ошибку через errors.As, а не утверждением типа, по той же причине. errors.As принимает указатель на переменную нужного типа.
  • Никогда не сопоставляйте текст err.Error(). Сообщения меняются между версиями, и сопоставление по тексту тогда молча ломается.

Определение собственных ошибок-маркеров и типов ошибок, а также объединение нескольких ошибок через errors.Join, разобраны на странице о собственных ошибках.

Обрабатывайте ошибку один раз

Ошибку нужно обработать ровно один раз. Обработка это одно из: вернуть её (обычно обёрнутой), залогировать и продолжить, повторить попытку или превратить в ответ пользователю. Сделать два из этого сразу самый частый баг с ошибками в коде на Go.

// Wrong: logged here, and returned, so it is logged again by every caller.
if err != nil {
	log.Printf("could not fetch user: %v", err)
	return err
}

// Right: add context and return. The top of the program logs once.
if err != nil {
	return fmt.Errorf("fetch user %d: %w", id, err)
}

Если логировать и возвращать, один и тот же сбой появляется в логах несколько раз, и каждый раз с меньшим контекстом, чем в итоговом сообщении. Пусть ошибки поднимаются до места, которое может решить, что делать (HTTP-обработчик, main, цикл воркера), и логируются там.

Где ошибки заканчивают путь

Наверху программы кто-то должен отреагировать на ошибку. В main это обычно значит напечатать её и выйти с ненулевым кодом:

Без аргументов программа печатает в stderr error: usage: app <name> и завершается с кодом 1 (введите имя в панели Args, чтобы увидеть другой путь). Если main сохраняет такую форму, а настоящая работа идёт в run, программу легко тестировать, а инструкции defer внутри run всё равно выполнятся, ведь os.Exit пропускает отложенные вызовы.

В HTTP-сервере вершиной является обработчик: он превращает ошибку в код статуса и безопасное сообщение для клиента, а подробное сообщение логирует для вас.

Какие ошибки можно игнорировать, а какие нельзя

Игнорировать ошибку иногда правильно, но делайте это явно через _, чтобы читатель понимал, что это решение:

_ = conn.SetDeadline(t) // best effort

Некоторые вызовы на практике не могут завершиться неудачей (strings.Builder.WriteString, bytes.Buffer.Write). Другие выглядят безобидно, но такими не являются: Close для файла, в который вы писали, может сообщить, что данные так и не попали на диск, а json.Marshal падает на каналах и функциях. Если сомневаетесь, проверяйте.

Линтер errcheck (входит в golangci-lint) сообщает о непроверенных ошибках. Сам go vet их не отмечает.

Ошибки и паники

В Go есть и panic, но это не система исключений. Используйте ошибки для всего, что может пойти не так при нормальной работе: неверный ввод, отсутствующие файлы, сбои сети. Панику оставьте для багов (невозможное состояние, нарушенный инвариант) и для сбоев при запуске, когда продолжать бессмысленно. Библиотека почти никогда не должна паниковать через свой API. См. panic и recover.

Как уменьшить повторение

if err != nil многословен, и предложения добавить для него новый синтаксис неоднократно отклонялись; в 2025 году команда Go объявила, что больше не занимается изменением синтаксиса обработки ошибок. Несколько шаблонов уменьшают шум в рамках языка:

  • Ранний возврат и маленькие функции. Большая часть повторений идёт от длинных функций со множеством шагов.
  • Липкая ошибка. Для последовательности записей сохраняйте первую ошибку в поле структуры, а последующие вызовы после этого пусть ничего не делают. Так работает bufio.Writer: ошибку проверяют один раз после Flush.
  • Одна обёртка на функцию. Отложенное замыкание над именованным результатом может добавить один и тот же контекст ко всем ошибкам, которые возвращает функция (см. defer).

Частые ошибки

  • Использование значения, когда err не nil. Сначала проверьте, потом используйте.
  • Залогировать и вернуть. Выберите что-то одно.
  • Сравнение через == после оборачивания. Используйте errors.Is.
  • Потеря причины из-за %v. Используйте %w, если только скрыть причину не является целью.
  • Возврат типизированного nil-указателя как error. var e *MyErr; return e для вызывающего кода не nil. Возвращайте литерал nil.
  • Сообщения с заглавной буквы или с точкой. errors.New("Failed to connect.") плохо читается после оборачивания. Пишите connect to db: ....

Часто задаваемые вопросы

Как устроена обработка ошибок в Go?

Функции, которые могут завершиться неудачей, возвращают error последним результатом. Вызывающий код сразу его проверяет: v, err := f(); if err != nil { return err }. error это обычное значение интерфейса с одним методом Error() string, а nil означает успех. Исключений нет.

Есть ли в Go try/catch?

Нет. В Go нет исключений и нет try/catch. Ожидаемые сбои возвращаются значениями error и проверяются через if err != nil. panic и recover существуют, но они для багов и неустранимых состояний, а не для обычного потока ошибок.

Как вернуть ошибку в Go?

Объявите error последним результатом и при успехе возвращайте nil. Создавайте ошибки через errors.New("message") для фиксированного текста или через fmt.Errorf("reading %s: %w", name, err), чтобы добавить контекст к полученной ошибке. При неудаче возвращайте нулевые значения для остальных результатов.

Чем %w отличается от %v в fmt.Errorf?

Оба помещают сообщение исходной ошибки в новую. %w ещё и оборачивает её, так что errors.Is и errors.As по-прежнему могут найти исходную ошибку. %v создаёт новую ошибку только с текстом. Используйте %w, когда вызывающему коду может понадобиться проверить причину, и %v, когда её нужно скрыть.

Как проверить, какая ошибка вернулась, в Go?

Используйте errors.Is(err, target), чтобы сравнить с ошибкой-маркером вроде io.EOF или os.ErrNotExist, и errors.As(err, &target), чтобы извлечь конкретный тип ошибки, например *fs.PathError. Обе функции проходят через обёрнутые ошибки. Не сравнивайте строки err.Error().

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ