Что проверяет errors.Is для ошибки, объединяющей несколько причин через errors.Join?
errors.Is проверяет все причины, входящие в ошибку, созданную через errors.Join, и возвращает true, если целевая ошибка найдена хотя бы в одной ветви. Поэтому объединённую ошибку нужно рассматривать как дерево причин, а не как одну линейную цепочку.
Традиционная модель обработки ошибок в Go долгое время описывала одну основную причину: ошибка могла оборачивать другую ошибку через Unwrap() error. Этого недостаточно, когда операция завершается сразу несколькими независимыми сбоями, например при массовой валидации или закрытии нескольких ресурсов.
В Go 1.20 появился errors.Join и поддержка методов Unwrap() []error. Это позволило сохранять несколько причин в одной ошибке, не сводя их к одной произвольно выбранной причине.
Предположим, операция обработала несколько объектов и обнаружила разные ошибки: нарушение формата в одном объекте и отсутствие доступа к другому. Если вернуть только первую ошибку, вызывающий код потеряет часть диагностической информации.
Если же объединить причины, но затем проверять результат сравнением текста или рассчитывать на одну линейную цепочку, можно ошибочно решить, что определённая причина отсутствует. Это влияет на выбор HTTP-статуса, повтор операции, метрики и уведомления.
errors.Join создаёт ошибку, которая хранит ненулевые переданные ошибки и предоставляет их через Unwrap() []error. При вызове errors.Is Go рекурсивно обходит дерево обёрток и считает проверку успешной, если целевая ошибка совпала с узлом или была найдена в любой дочерней ветви.
В примере errors.Is не сравнивает итоговый текст объединённой ошибки с текстом errAccess. Он обходит составные причины и находит нужный объект в одной из ветвей.
Нулевые ошибки игнорируются: если все переданные значения равны nil, результатом errors.Join будет nil. Это удобно для накопления ошибок, но вызывающий код должен понимать, что объединение может содержать несколько независимых причин.
Преимущество подхода — сохранение полной информации и совместимость со стандартными проверками errors.Is и errors.As. Компромисс заключается в том, что потребитель должен быть готов к множественным причинам: одна проверка может одновременно успешно определять несколько классов ошибок, а порядок причин не должен использоваться как часть API без явной гарантии.
Проверка по строке остаётся неправильной: формат представления объединённой ошибки предназначен для диагностики, а не для программной логики. Если вызывающему коду нужен особый смысл ошибки, его следует представить отдельным sentinel-значением или типом и включить в объединение.
Сервис импортирует пакет данных и выполняет независимую проверку каждой записи. Вариант с немедленным возвратом первой ошибки прост и дёшев, но заставляет пользователя исправлять пакет по одному дефекту за запуск. Вариант с ручным формированием одной текстовой строки показывает все проблемы, но лишает вызывающий код надёжной проверки через errors.Is и errors.As.
Выбран вариант с накоплением структурированных ошибок и последующим errors.Join. Каждая причина сохраняет собственный тип или sentinel-значение, а внешний слой может одновременно показать полный отчёт и проверить, присутствует ли, например, ошибка доступа. Результат — одна операция возвращает все найденные дефекты, не превращая машинную обработку в разбор текста.
1. Вопрос: Обязан ли errors.Is найти только непосредственные причины, переданные в errors.Join?
Нет. Он обходит не только непосредственные элементы объединения, но и ошибки, вложенные в них через обычный Unwrap() error или дальнейший Unwrap() []error. Поэтому причина может находиться на любой глубине дерева.
2. Вопрос: Что произойдёт, если одна и та же причина окажется в нескольких ветвях?
errors.Is всё равно вернёт true, как только найдёт совпадение. Проверка отвечает на вопрос о наличии причины, а не подсчитывает количество её появлений и не сообщает, в какой именно ветви она находилась.
3. Вопрос: Можно ли по errors.Join определить единственную главную ошибку?
Надёжно — нет. Семантика errors.Join сохраняет набор причин, но не назначает одну из них главной. Если порядок, приоритет или отдельная классификация важны для API, их нужно выразить явно: например, отдельным типом результата или заранее определённым правилом выбора, а не извлекать случайно из текста или позиции элемента.