При агрегировании результатов нескольких операций какое значение возвращает errors.Join, если все переданны...

При агрегировании результатов нескольких операций какое значение возвращает errors.Join, если все переданные ошибки равны nil?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

errors.Join возвращает nil, если все переданные ошибки равны nil или аргументы отсутствуют. Ненулевой результат появляется только при наличии хотя бы одной ненулевой ошибки.

Исторический контекст

До появления стандартного errors.Join разработчики обычно вручную выбирали одну ошибку, создавали собственные агрегирующие типы или использовали сторонние пакеты. В Go 1.20 появилась стандартная поддержка объединения нескольких причин без потери их доступности для errors.Is и errors.As.

Постановка проблемы

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

Это особенно опасно для функций, которые возвращают результат errors.Join напрямую: корректное поведение зависит от того, что nil-ошибки не должны создавать ошибку сами по себе.

Подробное решение

errors.Join игнорирует все аргументы, равные nil. Если после этого не остаётся ни одной ошибки, функция возвращает nil.

Если хотя бы одна ошибка ненулевая, возвращается ненулевая агрегированная ошибка. При этом отдельные причины сохраняются: агрегатор предоставляет механизм Unwrap() []error, поэтому errors.Is и errors.As могут искать совпадения среди всех объединённых причин.

package main import ( "errors" "fmt" ) func main() { var first error var second error joined := errors.Join(first, second) fmt.Println(joined == nil) // true }

Дополнительная ручная проверка вида «если все ошибки nil, вернуть nil» для самого результата errors.Join не требуется. Однако перед объединением нужно убедиться, что в список не попадают неошибочные значения, ошибочно созданные как ненулевые обёртки.

Ситуация из практики

Сервис выполняет несколько независимых проверок перед публикацией данных. Нужно сообщить вызывающему коду все найденные проблемы, а при полном успехе вернуть nil.

Первый вариант — вернуть только первую ошибку. Он проще, но скрывает остальные проблемы и заставляет клиента исправлять их по одной.

Второй вариант — вручную собирать текст всех ошибок. Это даёт полный отчёт, но теряет структурированные причины: errors.Is и errors.As больше не смогут надёжно определить исходные ошибки.

Выбранный вариант — передать все результаты в errors.Join. Ненулевые причины сохраняются для программной обработки, а если все проверки успешны, функция получает обычный nil. В результате API одновременно поддерживает массовый сбор ошибок и корректную семантику успеха.

Что кандидаты часто упускают

  1. Что произойдёт, если среди аргументов errors.Join одна ошибка ненулевая, а остальные равны nil?

    errors.Join вернёт ненулевую ошибку, содержащую единственную реальную причину. nil-аргументы не добавляют самостоятельных причин и не влияют на результат.

    Поэтому вызывающему не нужно предварительно фильтровать nil-значения только ради корректности Join, хотя фильтрация может быть полезна для оптимизации или для отдельной бизнес-логики.

  2. Можно ли после errors.Join найти исходную ошибку через errors.Is?

    Да. Агрегированная ошибка сохраняет причины и предоставляет Unwrap() []error. errors.Is обходит объединённые причины и возвращает true, если хотя бы одна из них соответствует искомой ошибке.

    Сравнивать текст результата Error() для этого нельзя: текст предназначен прежде всего для диагностики, а не для программной логики.

  3. Зачем проверять результат errors.Join, если список ошибок формируется динамически?

    Проверка результата нужна не для обработки случая «все аргументы nil» — в этом случае Join уже возвращает nil. Она может понадобиться, если вокруг агрегатора есть дополнительная логика: например, нужно записать метрику только при наличии ошибок или выбрать иной статус ответа.

    Если специальной логики нет, достаточно вернуть результат errors.Join напрямую. Это уменьшает дублирование проверок и сохраняет стандартную семантику объединения ошибок.