Интерфейсы в Go

Теория: Интерфейс error и кастомные ошибки

Во многих языках программирования ошибки реализованы через исключения. В Go подход другой: ошибки — это обычные значения. Для этого в стандартной библиотеке определён интерфейс error. Он очень простой: в нём всего один метод — Error() string. Этот метод возвращает строку с описанием ошибки. Если функция может завершиться неудачно, по соглашению она возвращает ошибку как последнее значение. Если всё прошло успешно — вместо ошибки возвращается nil. Рассмотрим простую функцию, которая делит одно число на другое:

func divide(a, b int) (int, error) {
	if b == 0 {
		return 0, errors.New("нельзя делить на ноль")
	}
	return a / b, nil
}

Если деление возможно — возвращается результат и nil. Если нет — 0 и ошибка. Вызов функции выглядит так:

result, err := divide(10, 0)
if err != nil {
	fmt.Println("ошибка:", err)
	return
}
fmt.Println("результат:", result)

В этом примере используется errors.New(), которая создаёт простую строковую ошибку. Она уже реализует интерфейс error и подходит для возврата из функций. Если требуется добавить к ошибке дополнительный контекст, используется функция fmt.Errorf(). Она работает как fmt.Sprintf(), но дополнительно поддерживает оборачивание ошибок. Чтобы вложить другую ошибку, применяется специальный маркер %w:

baseErr := errors.New("токен истёк")
err := fmt.Errorf("ошибка авторизации: %w", baseErr)

Теперь err содержит и текст "ошибка авторизации", и вложенную причину — истёкший токен. Такая обёртка сохраняет исходную ошибку внутри, что позволяет в дальнейшем её извлечь. Но в реальных проектах часто возникает необходимость не просто описывать ошибку строкой, а передавать с ней дополнительные данные: ID пользователя, имя поля, код операции. В таких случаях строка становится неудобной. Go предлагает для этого более гибкий инструмент — пользовательские типы ошибок. Пользовательский тип ошибки — это структура, которая реализует метод Error() string. Этого достаточно, чтобы она удовлетворяла интерфейсу error. Вместо строки такая структура может хранить всё, что нужно: идентификаторы, параметры, состояние. Вот пример пользовательского типа:

type PermissionError struct {
	User   string
	Action string
}
func (e PermissionError) Error() string {
	return fmt.Sprintf("пользователь %s не может выполнить действие: %s", e.User, e.Action)
}

Это и есть пользовательский тип ошибки. Он описывает ситуацию, когда у пользователя нет прав. Ошибка содержит не просто текст, а полезные данные. Её можно вернуть из функции как error, а потом извлечь и обработать отдельно. Похожим образом можно описать ошибку, если в системе не найден товар:

type ProductNotFoundError struct {
	ID string
}
func (e ProductNotFoundError) Error() string {
	return fmt.Sprintf("товар с ID %s не найден", e.ID)
}

Такие ошибки удобны, потому что они не теряют информацию. В них можно положить ID, имя пользователя, параметры запроса — и при этом они остаются совместимыми с интерфейсом error. Когда функция возвращает ошибку, она имеет тип error, и в момент вызова не всегда понятно, что за ошибка пришла. Это может быть простая строка или вложенный пользовательский тип. Чтобы точно понять, что за ошибка внутри, в Go предусмотрены специальные функции: errors.As() и errors.Is(). Функция errors.As() позволяет извлечь ошибку нужного типа из интерфейсного значения:

var perr PermissionError
if errors.As(err, &perr) {
	// err действительно PermissionError, доступен perr.User
}

Проверка через errors.As() безопасна: если тип не совпадает, ничего не произойдёт — функция просто вернёт false, и выполнение продолжится. Если ошибка была обёрнута через fmt.Errorf() с %w, errors.As() сам найдёт нужный тип в цепочке. Если нужно проверить не тип, а конкретное значение ошибки — например, заранее определённую переменную ErrTokenExpired — используется errors.Is():

if errors.Is(err, ErrTokenExpired) {
	// ошибка связана с истечением токена
}

Обе функции — errors.As() и errors.Is() — умеют проходить по всей цепочке вложенных ошибок. errors.As() извлекает значение нужного типа, а errors.Is() сравнивает с конкретным значением. Это позволяет точно различать ошибки и обрабатывать их по смыслу, а не по тексту. Чтобы errors.As() и errors.Is() могли находить вложенные ошибки, при оборачивании обязательно нужно использовать %w. Если вместо него указать %v, ошибка будет просто отформатирована как текст, но не вложена внутрь. В этом случае errors.As() и errors.Is() не смогут её найти — для них она как будто потерялась в строке. Система ошибок в Go строится на простой идее: ошибка — это обычное значение. Оно может быть простой строкой, пользовательской структурой с контекстом или результатом вложенного оборачивания. Благодаря интерфейсу error, пользовательским типам и функциям errors.Is() и errors.As(), ошибки становятся не просто сообщениями, а полноценной частью архитектуры. Их можно логировать, показывать пользователю, точно различать по типу и обрабатывать с учётом контекста. Такой подход делает код предсказуемым, а работу с ошибками — надёжной и управляемой.

Рекомендуемые программы

+7 800 100 22 47

бесплатно по РФ

+7 495 085 21 62

бесплатно по Москве

108813 г. Москва, вн.тер.г. поселение Московский,
г. Московский, ул. Солнечная, д. 3А, стр. 1, помещ. 20Б/3
ОГРН 1217300010476
ИНН 7325174845