Когда сравнение ошибок через == проверяет идентичность, а не только совпадение смысла?
Оператор == для ошибок проверяет равенство значений интерфейса, а не смысловую эквивалентность ошибки. Он надёжен при сравнении с одним и тем же сравнимым sentinel-значением, но не подходит для ошибок, созданных заново, обёрнутых или содержащих несравнимые данные.
Для проверки принадлежности ошибки к известной причине следует использовать errors.Is, если важна семантика ошибки, а не её точная идентичность.
В Go ошибки представлены обычными значениями, реализующими интерфейс error. В языке нет обязательной иерархии исключений, поэтому разработчикам понадобились простые способы различать заранее известные причины и добавлять контекст.
Изначально часто использовали sentinel-ошибки и прямое сравнение через ==. С появлением стандартной поддержки wrapping в Go 1.13 появились errors.Is и errors.As, позволяющие проверять смысл ошибки или извлекать её тип через цепочку обёрток.
Одинаковый текст сообщения не означает, что ошибки являются одним и тем же значением. Например, два вызова создания ошибки с одинаковым текстом обычно дают разные экземпляры, а добавление контекста создаёт новую обёртку.
Прямое сравнение может поэтому ошибочно сообщить, что причина отсутствует. Дополнительный риск возникает, если динамическое значение внутри интерфейса содержит несравнимые поля: сравнение таких интерфейсов может завершиться паникой.
Интерфейсное значение в Go содержит динамический тип и динамическое значение. При сравнении двух интерфейсов через == сначала сопоставляются их динамические типы, а затем сравниваются значения; динамический тип должен поддерживать сравнение.
Для sentinel-ошибки сравнение корректно, если функция возвращает именно этот объект без обёртки:
Переменная ErrNotFound содержит один указатель на значение ошибки, поэтому сравнение с ней работает для того же экземпляра. Обёртка — другое значение, даже если она хранит исходную ошибку внутри; errors.Is специально проходит по цепочке wrapping и проверяет логическую принадлежность причины.
Ошибки, созданные отдельными вызовами errors.New, не становятся равными из-за одинакового текста. Сравнение структурных ошибок через == возможно только при сравнимом типе, но оно проверяет значения полей и не гарантирует, что такое сравнение отражает публичный контракт ошибки.
Если динамический тип ошибки содержит, например, срез или отображение и реализует Error методом значения, сравнение двух интерфейсов с таким типом может вызвать панику. Поэтому тип ошибки не должен случайно использоваться через ==, если его сравнимость не является частью замысла.
Сервис возвращает ошибку отсутствия записи. Сначала вызывающий код сравнивает результат через == с sentinel-ошибкой. После добавления в репозитории контекста через wrapping проверка перестаёт срабатывать, и отсутствие записи ошибочно превращается в обычную внутреннюю ошибку.
Рассматривались три варианта:
Выбран третий вариант: sentinel-ошибка остаётся частью согласованного контракта, а внутренние слои свободно добавляют контекст. В результате лог содержит подробную цепочку причины, а вызывающий код стабильно распознаёт отсутствие записи.
Не всегда. Если два интерфейса содержат один и тот же сравнимый экземпляр ошибки, сравнение даст true, даже если текст совпал лишь случайно. При двух отдельных вызовах errors.New создаются разные значения, поэтому одинаковые сообщения сами по себе равенства не дают.
Да. Если динамическое значение интерфейса имеет несравнимый тип, например структуру со срезом, сравнение интерфейсов с такими значениями вызывает панику во время выполнения. Наличие метода Error не делает тип автоматически сравнимым.
Практический вывод: не следует полагаться на == для произвольных пользовательских типов ошибок. Для семантической проверки причины лучше определить подходящий контракт и использовать errors.Is или errors.As.
Нет. Sentinel удобен, когда вызывающему коду действительно нужно ветвиться по стабильной категории: например, отличать отсутствие ресурса от ошибки доступа. Для чисто внутренних сбоев достаточно вернуть описательную ошибку с контекстом.
Избыточное количество публичных sentinel-ошибок связывает API с деталями реализации и усложняет эволюцию пакета. Поэтому sentinel следует вводить только для тех причин, которые являются устойчивой частью договора между слоями.