Обработка ошибок в Go: почему нет исключений и как с этим жить
Go сознательно отказался от механизма исключений, привычного разработчикам на Python, Java или C#. Разбираем, какую проблему решал этот выбор, как устроена модель ошибок через возвращаемые значения и где она ломается на практике.

Отказ от исключений — решение, а не ограничение
В 2009 году, когда Go вышел в открытый доступ, разработчики из Google — Роб Пайк, Кен Томпсон и Роберт Гризмер — сделали осознанную ставку: никаких try/catch. Ошибка — это обычное значение, которое функция возвращает вторым аргументом. Вызывающий код обязан его проверить. Если не проверил — компилятор не остановит, но линтеры и ревьюеры остановят.
Для тех, кто пришёл из Java или Python, это выглядит как шаг назад. На деле — это другая философия контроля потока: поток исполнения всегда линеен, ошибка не «летит» через пять уровней стека, и вы всегда видите, в какой строке обработка произошла. Ниже — разбор механизма, его цены и мест, где он ломается первым.
Почему исключения не попали в язык
У создателей Go был опыт работы с большими кодовыми базами на C++ внутри Google. Две претензии к исключениям повторяются в публичных выступлениях Пайка и в официальном FAQ Go:
- Скрытый поток управления. Исключение может пролететь через десятки функций, и ни одна из них не обязана его документировать (в отличие от Java checked exceptions, но и те разработчики массово обходят через RuntimeException).
- Тяжёлый стек-трейс. Создание объекта исключения с захватом стека — дорогая операция. В высоконагруженных сервисах, где ошибки — не исключительная ситуация, а нормальная часть потока (таймаут, невалидный ввод, отсутствие записи), платить за каждую ошибку стек-трейсом расточительно.
Третий аргумент — читаемость при ревью. Когда каждая ошибка обрабатывается в точке возврата, ревьюер видит все ветки: что происходит при успехе, что при провале. Нет нужды держать в голове, какой catch на каком уровне поймает что-то неожиданное.
Errors are values. — Rob Pike, «Errors are values», блог Go, 2015
Как устроена модель ошибок в Go
Интерфейс error
Вся система построена на одном интерфейсе из стандартной библиотеки (Go 1.22+, хотя интерфейс не менялся с первой версии):
type error interface {
Error() string
}
Любой тип, реализующий метод Error() string, считается ошибкой. Это позволяет создавать собственные типы с дополнительными полями — HTTP-кодом, внутренним кодом, контекстом запроса.
Идиома «if err != nil»
Типичный вызов выглядит так:
// Go 1.22+
result, err := doSomething(ctx)
if err != nil {
return fmt.Errorf("не удалось выполнить операцию: %w", err)
}
// работаем с result
Глагол %w (от слова wrap) появился в Go 1.13. Он оборачивает оригинальную ошибку, сохраняя цепочку. Без него — %v — ошибка форматируется в строку, и связь с оригиналом теряется.
errors.Is и errors.As — распаковка цепочки
Начиная с Go 1.13 стандартная библиотека предлагает два способа проверки обёрнутых ошибок:
| Функция | Что делает | Когда использовать |
|---|---|---|
errors.Is(err, target) | Рекурсивно разворачивает цепочку и сравнивает по значению | Проверить конкретный сигнальный экземпляр: io.EOF, sql.ErrNoRows, context.Canceled |
errors.As(err, &target) | Рекурсивно разворачивает и пробует привести к типу | Достать поля из кастомной ошибки: HTTP-код, имя поля валидации |
errors.Unwrap(err) | Возвращает следующую ошибку в цепочке (один уровень) | Редко нужна напрямую — Is и As вызывают её сами |
Пример с кастомным типом:
// Go 1.22+
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("поле %s: %s", e.Field, e.Message)
}
// Вызывающий код
var ve *ValidationError
if errors.As(err, &ve) {
log.Printf("ошибка валидации в поле %s", ve.Field)
}
panic и recover — не замена исключениям
В Go есть механизм panic/recover, и новички из мира исключений тянутся к нему как к знакомому. Это ловушка.
panic предназначен для ситуаций, из которых программа не должна продолжать работу: обращение к nil-указателю, выход за границу слайса, нарушение инварианта, которое указывает на баг. Стандартная библиотека паникует крайне редко — например, при дублировании маршрутов в http.ServeMux.
recover перехватывает панику, но только внутри defer-функции той же горутины. Если горутина запаниковала и никто не вызвал recover — падает весь процесс. Нет глобального catch, нет аналога Thread.UncaughtExceptionHandler.
Правило в продакшн-коде: recover допустим в двух местах — в middleware HTTP-сервера (чтобы одна паника не убила весь сервис) и на границе горутины в воркер-пуле. Везде остальное — обычные ошибки через возвращаемое значение.
Где модель ломается первым
Вербозность
Самая частая претензия к Go — бесконечные блоки if err != nil. В функции из десяти вызовов внешних сервисов половина строк — обработка ошибок. Читаемость падает не потому, что модель плоха, а потому, что язык не даёт синтаксического сахара для цепочки. Были предложения (proposals) добавить оператор check или try — все до сих пор отклонены. В Go 1.22 ничего не изменилось; за обновлениями стоит следить в трекере предложений Go.
Потерянный контекст
Если оборачивать ошибку через %v вместо %w, цепочка рвётся: errors.Is и errors.As больше не найдут оригинал. В большом проекте это обнаруживается, когда в проде начинают прилетать строки вроде «не удалось обработать запрос: не удалось получить данные: connection refused» — и определить программно, что это именно сетевая ошибка, невозможно.
Игнорирование ошибок
Компилятор Go не заставляет обрабатывать возвращённую ошибку. Можно написать doSomething(ctx) без присвоения второго значения, и код скомпилируется. Линтер errcheck (и более мощный golangci-lint с включённым правилом errcheck) ловит такие случаи — но только если его настроили в CI. Без линтера необработанные ошибки молча съедаются, и система ведёт себя непредсказуемо.
Горутины и ошибки
Горутина не может вернуть значение вызывающему коду. Ошибку нужно передать явно — через канал, через errgroup.Group из пакета golang.org/x/sync/errgroup или через другую структуру синхронизации. Забытый канал ошибок — частый источник «тихих» сбоев: горутина завершилась с ошибкой, но основной поток об этом не узнал.
Сноска: errgroup — пакет из расширенной стандартной библиотеки Go (golang.org/x/sync), позволяющий запустить группу горутин и дождаться первой ошибки. Он не входит в основной модуль std, но поддерживается командой Go.
Приёмы, снижающие боль
- Всегда оборачивайте через
%w. Это сохраняет возможность проверки черезerrors.Is/errors.Asна любом уровне стека. - Создавайте sentinel-ошибки на уровне пакета.
var ErrNotFound = errors.New("not found")позволяет вызывающему коду реагировать на конкретную ситуацию, не парся строки. - Используйте кастомные типы, когда нужны данные. Код ответа, имя поля, ID ресурса — всё, что поможет вызывающему принять решение без разбора текста.
- Включите errcheck в CI. Без линтера модель ошибок Go держится на дисциплине, а дисциплина в команде из двадцати человек — ненадёжная защита.
- Для группы горутин — errgroup. Он отменяет контекст при первой ошибке и возвращает именно её, а не nil.
Что делать дальше
Если вы только перешли на Go — настройте golangci-lint с правилами errcheck, wrapcheck и goerr113 до того, как кодовая база вырастет. Привыкните оборачивать ошибки с контекстом действия: не «failed», а «не удалось загрузить профиль пользователя %d». Прочитайте официальный пост Errors are values — в нём Пайк показывает шаблон errWriter, который сокращает повторяющиеся проверки без магии. Следите за предложениями по улучшению обработки ошибок в трекере Go — вопрос синтаксического сахара обсуждается активно и может измениться в будущих версиях.
Аналитик звонков: Разбор записи звонка по этапам сделки: где сорвалось, что упущено, три исправляемые рекомендации с цитатами.
Спросить за 90 ₽Источники: Go Blog — Errors are values (Rob Pike, 2015), Go Blog — Working with Errors in Go 1.13
Частые вопросы
Почему Go не добавит исключения в будущих версиях?
Можно ли использовать panic/recover вместо возврата ошибок?
Что будет, если я забуду проверить err?
Чем %w отличается от %v при оборачивании ошибки?
Как передать ошибку из горутины обратно в вызывающий код?
Есть ли в Go аналог стек-трейса при ошибке?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.