Ошибки это значения
Функция в 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().