Как меняется семантика обёртки ошибки, если форматирование содержит несколько спецификаторов %w?
В Go несколько спецификаторов %w создают ошибку с несколькими дочерними причинами, то есть дерево ошибок, а не обычную линейную цепочку. errors.Is и errors.As рекурсивно обходят все ветви, поэтому вызывающий может найти любую сохранённую причину.
Изначально стандартная модель wrapping предполагала одну исходную причину через метод Unwrap() error. Этого достаточно для последовательного добавления контекста, но недостаточно, когда одна операция завершается несколькими независимыми ошибками.
В Go 1.20 появилась поддержка объединения нескольких причин: через errors.Join и через несколько %w в fmt.Errorf. Это позволило сохранять машинно-обрабатываемые причины без склеивания их только в текст сообщения.
Представим операцию, которая одновременно проверяет несколько независимых ресурсов. Если вернуть только первую ошибку, остальные причины потеряются; если объединить сообщения вручную, errors.Is и errors.As больше не смогут надёжно определить исходные ошибки.
При нескольких %w возникает другая особенность: результатом становится не цепочка, а дерево. Поэтому поиск типа через errors.As может зависеть от порядка ветвей, если несколько причин подходят под один и тот же тип.
fmt.Errorf с несколькими %w возвращает ошибку, реализующую Unwrap() []error. Каждая непустая причина становится дочерней ошибкой. Обход дерева средствами errors.Is и errors.As выполняется рекурсивно по ветвям; найденная подходящая причина завершает поиск.
Такой механизм следует применять для независимых причин одного результата: например, ошибок нескольких параллельных проверок. Он не означает, что ошибки произошли последовательно; для последовательного контекста лучше использовать одну обычную обёртку с одним %w.
Сообщение ошибки при этом остаётся предназначенным для человека, а errors.Is и errors.As работают с сохранёнными объектами причин. Код, использующий errors.As, должен учитывать возможность нескольких подходящих ветвей и не полагаться на случайный порядок, если порядок не является частью контракта.
Сервис запускает проверку конфигурации и проверку доступности резервного хранилища. Возврат только первой ошибки упрощает код, но скрывает часть диагностики. Ручная конкатенация текстов показывает обе проблемы оператору, однако лишает вызывающий код возможности проверить конкретную категорию ошибки.
Выбран вариант с несколькими %w: он одновременно добавляет общий контекст и сохраняет обе причины для errors.Is и errors.As. Если же требуется просто агрегировать набор ошибок без собственного сообщения, уместнее errors.Join; если ошибки образуют последовательную цепочку причин, следует использовать одну обёртку с одним %w.
%w обычной цепочкой обёрток?Нет. Обычная цепочка имеет одну следующую ошибку и представляется Unwrap() error. Несколько %w создают несколько дочерних ошибок через Unwrap() []error, поэтому структура результата является деревом.
errors.As, если две ветви содержат подходящий тип?Поиск идёт по дереву в определённом порядке обхода, и возвращается первая подходящая ошибка. Поэтому нельзя без необходимости строить логику на выборе между несколькими одинаково подходящими значениями; при таком API порядок ветвей становится практически значимым.
Текст сообщения нестабилен: его могут изменить ради читаемости или локализации. Сохранённые причины позволяют программно определить категорию, извлечь структурированные данные через errors.As и выбрать действие без зависимости от формулировки текста.