Перейти к основному содержимому

Обработка ошибок в Go

Разработчику Архитектору
Сначала — общая теория

Ошибки, исключения и отказоустойчивость — в Go нет исключений: ожидаемый сбой — значение error; panic — для невосстановимых инвариантов (аналог фатального сбоя), не для бизнес-валидации.


Обработка ошибок в Go

Интерактивное демо — в Go нет исключений, ошибки — значения error; смотрите сценарий "код ошибки" и стек. В коде: if err != nil. Подробнее: ошибки и исключения.

Play ITЗагрузка интерактивного демо…


Основные принципы обработки ошибок в Go

  1. Ошибки — это значения.
    Тип error — это встроенный интерфейс:
type error interface {
Error() string
}

Разбор:

  • type error interface задаёт контракт для ошибок в Go на уровне языка.
  • Единственный метод Error() string определяет текстовое представление ошибки.
  • Любой тип, который реализует этот метод, автоматически становится совместимым с error.
  • Благодаря интерфейсу ошибки ведут себя как обычные значения и участвуют в типобезопасной логике программы.
  1. Функции возвращают ошибку как последнее значение (по соглашению):
func doSomething() (Result, error)

Разбор:

  • Функция возвращает сразу два значения: полезный результат Result и ошибку error.
  • Размещение error последним делает сигнатуры единообразными по всей экосистеме Go.
  • В корректном сценарии возвращается nil во второй позиции, при проблеме — заполненная ошибка.
  • Такая форма помогает писать стандартный шаблон обработки через if err != nil.
  1. Нет try/catch, throw, raise.
    Ошибки проверяются явно с помощью условного оператора:
if err != nil {
// обработка ошибки
}

Разбор:

  • Проверка err != nil явно показывает точку, где выполнение может перейти в ветку обработки сбоя.
  • Внутри блока обычно выполняют логирование, оборачивание ошибки через %w или ранний return.
  • Явная обработка уменьшает "магичность" потока управления и делает код проще для ревью.
  • Разработчик контролирует каждую ошибку локально, без скрытого механизма исключений.
  1. Паника (panic) существует, но не предназначена для обычной обработки ошибок.
    • panic(v interface{}) прерывает нормальное выполнение.
    • recover() может быть вызван в отложенной функции (defer) для перехвата паники.
    • Используется только для некорректируемых ошибок (например, нарушение инвариантов, ошибки инициализации).

Пример типичного паттерна result, err:

func parseAge(s string) (int, error) {
age, err := strconv.Atoi(s)
if err != nil {
return 0, fmt.Errorf("parse age %q: %w", s, err)
}
if age < 0 {
return 0, fmt.Errorf("age must be non-negative")
}
return age, nil
}

Разбор:

  • strconv.Atoi пытается преобразовать строку в число и возвращает ошибку при невалидном формате.
  • Ветвь if err != nil добавляет контекст через %w, чтобы исходную причину можно было проверить через errors.Is/As.
  • Бизнес-ограничение (age < 0) оформлено как отдельная ошибка, потому что это валидный формат, но некорректное значение.
  • Возврат age, nil обозначает успешный путь и завершённую валидацию.

Встроенные типы ошибок

Go не предоставляет иерархии стандартных классов ошибок. Однако в стандартной библиотеке определены некоторые конкретные типы, реализующие интерфейс error:


1. errors.errorString (неэкспортируемый)

  • Внутренний тип, используемый функцией errors.New("message").
  • Содержит только строковое сообщение.

2. Общие ошибки из стандартной библиотеки

  • io.EOF — сигнал конца потока (не ошибка в обычном смысле).
  • io.ErrUnexpectedEOF
  • io.ErrNoProgress
  • os.ErrInvalid
  • os.ErrPermission
  • os.ErrExist
  • os.ErrNotExist
  • syscall.Errno — системные ошибки (например, ENOENT, EACCES), которые также реализуют error.

3. Пакет errors (начиная с Go 1.13) предоставляет поддержку

  • Оборачивания ошибок: fmt.Errorf("...: %w", err)
  • Проверки цепочки ошибок: errors.Is(err, target), errors.As(err, &target)

Это позволяет строить цепочки ошибок с контекстом, но без иерархии типов.

Пример проверки цепочки ошибок:

Код ITЗагрузка примера кода…

Разбор:

  • os.ReadFile может вернуть системную ошибку файла, которая оборачивается в loadConfig.
  • fmt.Errorf(... %w ...) сохраняет исходную ошибку в цепочке и добавляет понятный уровень контекста.
  • errors.Is(err, os.ErrNotExist) проверяет всю цепочку, а не только верхний текст ошибки.
  • Ветвление по типу ошибки позволяет принимать разные решения: fallback для отсутствующего файла и отдельная обработка остальных сбоев.

Паники (аварийные остановки)

Хотя паники не являются "ошибками" в обычном смысле, они могут быть вызваны встроенным кодом:

  • Нарушение границ массива: index out of range
  • Разыменование нулевого указателя: invalid memory address or nil pointer dereference
  • Вызов метода у nil-интерфейса
  • Конкурентное изменение map без мьютекса: concurrent map writes
  • Переполнение стека: stack overflow
  • Ошибки при разыменовании интерфейса: interface conversion: ...

Эти ситуации вызывают panic, но не представляют собой именованные типы исключений — сообщение передаётся как строка или встроенная константа.


Нет типов ошибок

В Go нет списка "типов ошибок", аналогичного другим языкам, потому что:

  • Ошибки — это значения произвольных типов, реализующих интерфейс error.
  • Нет иерархии наследования.
  • Нет встроенных классов вроде IndexError, KeyError и т.п.
  • Стандартная библиотека использует семантические переменные-ошибки (os.ErrNotExist) или динамические сообщения.

Таким образом, вместо таксономии исключений в Go применяется дисциплина явной проверки возвращаемых значений и использование контекстуальных сообщений.

Если требуется классификация ошибок, разработчик сам определяет соответствующие типы:

type ValidationError struct {
Field string
Msg string
}

func (e ValidationError) Error() string {
return fmt.Sprintf("validation failed for %s: %s", e.Field, e.Msg)
}

Разбор:

  • ValidationError хранит структурированную информацию об ошибке валидации, а не только строку.
  • Поле Field указывает, где именно произошла ошибка, Msg описывает причину.
  • Метод Error() реализует интерфейс error, поэтому тип можно возвращать из функций как обычную ошибку.
  • fmt.Sprintf(...) формирует унифицированное сообщение для логов и ответов API.
  • Такой подход упрощает точечную обработку через errors.As, когда нужен доступ к полям конкретного типа.

Такой подход обеспечивает гибкость, но отказывается от глобальной иерархии ошибок.