Какой результат вернёт fmt.Errorf с единственным %w и аргументом nil?
fmt.Errorf("...: %w", nil) вернёт ненулевую ошибку, а не nil. Это важно: безусловное оборачивание необязательной ошибки может превратить успешное выполнение в ошибочное.
До появления оборачивания ошибок в Go 1.13 разработчики обычно добавляли контекст форматированием, но связь с исходной ошибкой для машинной обработки терялась. Спецификатор %w был введён, чтобы сохранить эту связь и позволить использовать errors.Is и errors.As.
При этом %w не является условным оператором: он формирует ошибку независимо от того, равен ли переданный аргумент nil.
Типичный риск возникает в функции-помощнике, которая добавляет контекст к результату другой функции. Если она оборачивает значение без проверки, успешный вызов может вернуть ненулевую ошибку.
В результате вызывающий код ошибочно завершит транзакцию, прервёт цепочку операций, запишет ложное сообщение в журнал или начнёт ненужные повторы. Проверка errors.Is(result, nil) также не спасает: для ненулевой ошибки она вернёт false.
При аргументе nil fmt.Errorf создаёт объект ошибки с указанным форматом. Внутренняя причина у такого объекта может быть nil, но сам объект ошибки существует, поэтому сравнение результата с nil ложно.
Минимальная демонстрация:
Правильный шаблон — проверять ошибку до оборачивания:
Так сохраняется важный контракт Go: nil означает отсутствие ошибки, а ненулевое значение — её наличие. Проверять нужно именно интерфейс error до вызова fmt.Errorf; отдельно следует помнить о типизированном nil, который уже может находиться в ненулевом интерфейсе.
Слой конфигурации возвращает ошибку чтения файла, а общий помощник добавляет имя подсистемы. Первый вариант безусловно вызывает fmt.Errorf и для успешного чтения возвращает ненулевую ошибку. Это решение проще визуально, но создаёт ложные сбои и может быть обнаружено только интеграционными тестами.
Второй вариант требует проверку nil в каждом месте вызова. Он корректен, но дублирует логику и повышает риск, что новый вызов забудет проверку.
Выбранное решение — сделать проверку частью общего помощника и покрыть её тестами для nil и ненулевой ошибки. В итоге контекст добавляется только при сбое, а вызывающий код получает предсказуемый контракт; при этом исходная причина остаётся доступной через errors.Is и errors.As.
%w использовать %v?Нет. fmt.Errorf в обоих случаях создаст ненулевую ошибку даже при nil-аргументе. Разница в другом: %w сохраняет исходную ошибку для обхода через errors.Is и errors.As, а %v только форматирует её текст.
err == nil внутри функции-обёртки?Для обычного nil этого достаточно. Но значение конкретного указательного типа, равное nil, может быть помещено в интерфейс error; сам интерфейс тогда будет ненулевым. Поэтому функция может корректно увидеть ненулевой error и обернуть его, хотя внутри находится типизированный nil. Это отдельная проблема формирования возвращаемых ошибок: обычно не следует возвращать типизированный nil как error.
nil?Такая ошибка сама по себе ненулевая, но её причина отсутствует. errors.Is не признает её равной nil, а поиск конкретной причины не найдёт исходный объект. Поэтому проверка nil должна выполняться до оборачивания, а не откладываться до обработки результата.