Обработка ошибок в Go
Ошибки, исключения и отказоустойчивость — в Go нет исключений: ожидаемый сбой — значение error; panic — для невосстановимых инвариантов (аналог фатального сбоя), не для бизнес-валидации.
Обработка ошибок в Go
Интерактивное демо — в Go нет исключений, ошибки — значения
error; смотрите сценарий "код ошибки" и стек. В коде:if err != nil. Подробнее: ошибки и исключения.
Play ITЗагрузка интерактивного демо…
Основные принципы обработки ошибок в Go
- Ошибки — это значения.
Типerror— это встроенный интерфейс:
type error interface {
Error() string
}
Разбор:
type error interfaceзадаёт контракт для ошибок в Go на уровне языка.- Единственный метод
Error() stringопределяет текстовое представление ошибки. - Любой тип, который реализует этот метод, автоматически становится совместимым с
error. - Благодаря интерфейсу ошибки ведут себя как обычные значения и участвуют в типобезопасной логике программы.
- Функции возвращают ошибку как последнее значение (по соглашению):
func doSomething() (Result, error)
Разбор:
- Функция возвращает сразу два значения: полезный результат
Resultи ошибкуerror. - Размещение
errorпоследним делает сигнатуры единообразными по всей экосистеме Go. - В корректном сценарии возвращается
nilво второй позиции, при проблеме — заполненная ошибка. - Такая форма помогает писать стандартный шаблон обработки через
if err != nil.
- Нет
try/catch,throw,raise.
Ошибки проверяются явно с помощью условного оператора:
if err != nil {
// обработка ошибки
}
Разбор:
- Проверка
err != nilявно показывает точку, где выполнение может перейти в ветку обработки сбоя. - Внутри блока обычно выполняют логирование, оборачивание ошибки через
%wили раннийreturn. - Явная обработка уменьшает "магичность" потока управления и делает код проще для ревью.
- Разработчик контролирует каждую ошибку локально, без скрытого механизма исключений.
- Паника (
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.ErrUnexpectedEOFio.ErrNoProgressos.ErrInvalidos.ErrPermissionos.ErrExistos.ErrNotExistsyscall.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, когда нужен доступ к полям конкретного типа.
Такой подход обеспечивает гибкость, но отказывается от глобальной иерархии ошибок.