Интерфейсы в Go
Теория: Интерфейс error и кастомные ошибки
Во многих языках программирования ошибки реализованы через исключения. В Go подход другой: ошибки — это обычные значения. Для этого в стандартной библиотеке определён интерфейс error. Он очень простой: в нём всего один метод — Error() string. Этот метод возвращает строку с описанием ошибки.
Если функция может завершиться неудачно, по соглашению она возвращает ошибку как последнее значение. Если всё прошло успешно — вместо ошибки возвращается nil.
Рассмотрим простую функцию, которая делит одно число на другое:
Если деление возможно — возвращается результат и nil. Если нет — 0 и ошибка. Вызов функции выглядит так:
В этом примере используется errors.New(), которая создаёт простую строковую ошибку. Она уже реализует интерфейс error и подходит для возврата из функций.
Если требуется добавить к ошибке дополнительный контекст, используется функция fmt.Errorf(). Она работает как fmt.Sprintf(), но дополнительно поддерживает оборачивание ошибок. Чтобы вложить другую ошибку, применяется специальный маркер %w:
Теперь err содержит и текст "ошибка авторизации", и вложенную причину — истёкший токен. Такая обёртка сохраняет исходную ошибку внутри, что позволяет в дальнейшем её извлечь.
Но в реальных проектах часто возникает необходимость не просто описывать ошибку строкой, а передавать с ней дополнительные данные: ID пользователя, имя поля, код операции. В таких случаях строка становится неудобной. Go предлагает для этого более гибкий инструмент — пользовательские типы ошибок.
Пользовательский тип ошибки — это структура, которая реализует метод Error() string. Этого достаточно, чтобы она удовлетворяла интерфейсу error. Вместо строки такая структура может хранить всё, что нужно: идентификаторы, параметры, состояние.
Вот пример пользовательского типа:
Это и есть пользовательский тип ошибки. Он описывает ситуацию, когда у пользователя нет прав. Ошибка содержит не просто текст, а полезные данные. Её можно вернуть из функции как error, а потом извлечь и обработать отдельно.
Похожим образом можно описать ошибку, если в системе не найден товар:
Такие ошибки удобны, потому что они не теряют информацию. В них можно положить ID, имя пользователя, параметры запроса — и при этом они остаются совместимыми с интерфейсом error.
Когда функция возвращает ошибку, она имеет тип error, и в момент вызова не всегда понятно, что за ошибка пришла. Это может быть простая строка или вложенный пользовательский тип. Чтобы точно понять, что за ошибка внутри, в Go предусмотрены специальные функции: errors.As() и errors.Is().
Функция errors.As() позволяет извлечь ошибку нужного типа из интерфейсного значения:
Проверка через errors.As() безопасна: если тип не совпадает, ничего не произойдёт — функция просто вернёт false, и выполнение продолжится. Если ошибка была обёрнута через fmt.Errorf() с %w, errors.As() сам найдёт нужный тип в цепочке.
Если нужно проверить не тип, а конкретное значение ошибки — например, заранее определённую переменную ErrTokenExpired — используется errors.Is():
Обе функции — errors.As() и errors.Is() — умеют проходить по всей цепочке вложенных ошибок. errors.As() извлекает значение нужного типа, а errors.Is() сравнивает с конкретным значением. Это позволяет точно различать ошибки и обрабатывать их по смыслу, а не по тексту.
Чтобы errors.As() и errors.Is() могли находить вложенные ошибки, при оборачивании обязательно нужно использовать %w. Если вместо него указать %v, ошибка будет просто отформатирована как текст, но не вложена внутрь. В этом случае errors.As() и errors.Is() не смогут её найти — для них она как будто потерялась в строке.
Система ошибок в Go строится на простой идее: ошибка — это обычное значение. Оно может быть простой строкой, пользовательской структурой с контекстом или результатом вложенного оборачивания. Благодаря интерфейсу error, пользовательским типам и функциям errors.Is() и errors.As(), ошибки становятся не просто сообщениями, а полноценной частью архитектуры. Их можно логировать, показывать пользователю, точно различать по типу и обрабатывать с учётом контекста. Такой подход делает код предсказуемым, а работу с ошибками — надёжной и управляемой.


