ПрограммированиеBackend разработчик

Как реализовано управление ошибками в Go, почему Go выбрал свою модель error handling, и какие лучшие практики использования ошибок в повседневном программировании?

Проходите собеседования с ИИ помощником Hintsage

Ответ.

История вопроса:

Go отличился от многих языков таким подходом к обработке ошибок, где ошибки — это значения, а не исключения. Такой дизайн был выбран ради прозрачности и простоты: всякий раз, когда что-то может пойти не так, функция возвращает ошибку явно как второе возвращаемое значение.

Проблема:

Многие новички пытаются применить к Go привычные паттерны try-catch, теряются в большом количестве проверок ошибок или не используют обёртывание ошибок для передачи контекста. Это приводит к потере информативности и трудному дебагу.

Решение:

В Go функция, которая может вернуть ошибку, объявляется так:

func doSomething() (ResultType, error) { // ... if somethingWrong { return nil, errors.New("something went wrong") } return result, nil }

Проверка ошибок — обязанность вызывающей стороны:

res, err := doSomething() if err != nil { log.Fatalf("process failed: %w", err) }

Go 1.13 и выше позволяют "оборачивать" ошибки с помощью fmt.Errorf("%w", err) для построения цепочек ошибок. Это улучшает диагностику и продвигает best practice контекстного описания ошибок.

Ключевые особенности:

  • Ошибки — это значения, возвращаемые функциями (нет try/catch)
  • Можно создавать собственные типы ошибок (используя интерфейс error)
  • Обёртывание ошибок делает дебаг и логирование прозрачнее

Вопросы с подвохом.

Зачем создавать свои типы ошибок, если можно просто возвращать errors.New("...")?

Правильный ответ: собственные типы ошибок позволяют не только сообщить текст ошибки, но и сохранить дополнительную информацию для дальнейшей обработки (например, коды возврата, контекст), а также реализовать более тонкую обработку через type assertion.

Пример:

type NotFoundError struct { Resource string } func (e NotFoundError) Error() string { return fmt.Sprintf("%s not found", e.Resource) } // Проверка if _, ok := err.(NotFoundError); ok { // обработка ошибки поиска }

Является ли panic хорошей заменой error для критических ошибок?

Нет, panic в Go применяется только в действительно безвыходных ситуациях — например, при ошибках самой программы (программных инвариантах), но не для обычных сбоев (например, не удалось открыть файл). Использование panic для сигнализации о рутинных ошибках — зло и приводит к нечитабельному, неуправляемому коду.

Что происxодит, если return пропустить обработку ошибки (err) во вложенных функциях?

Ошибка "теряется", код продолжает исполняться, что может привести к распространению ошибочного состояния. Всегда важно правильно обрабатывать каждый возврат ошибки.

Типовые ошибки и анти-паттерны

  • Игнорирование возвращаемых ошибок с помощью _ = ...
  • Использование panic/recover вместо error для потокового управления
  • Перенаправление неотправленных ошибок без сообщения контекста

Пример из жизни

Негативный кейс

Каждая функция просто возвращает errors.New(...), ошибки не завернуты, типы ошибок разные, но обработка всегда одна — логируется и выбрасывается. В результате лог-файлы наполнены неинформативными сообщениями, их невозможно отследить до исходной причины.

Плюсы:

  • Быстрое написание кода
  • Меньше кода на обработку

Минусы:

  • Плохая трассировка
  • Нет возможности фильтровать или различать ошибки по типу

Позитивный кейс

Используются обёртывания ошибок через fmt.Errorf("%w", err), собственные ошибки с полезными полями, проверки через errors.Is()/errors.As(). Благодаря этому, можно логировать с деталями, отделять бизнес-ошибки от сбоев окружения и писать надёжный retry-логика.

Плюсы:

  • Удобный дебаг
  • Более чистый код
  • Гибкая обработка ошибок

Минусы:

  • Больше кода и шаблонной логики
  • Необходимо поддерживать типы ошибок