Программирование GoОбработка ошибокРазработчик серверных приложений на Go

Какой результат вернёт fmt.Errorf с единственным %w и аргументом nil?

Какой результат вернёт fmt.Errorf с единственным %w и аргументом nil?

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

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

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 ложно.

Минимальная демонстрация:

package main import ( "errors" "fmt" ) func addContext(err error) error { return fmt.Errorf("read config: %w", err) } func main() { err := addContext(nil) fmt.Println(err == nil) // false fmt.Println(errors.Is(err, nil)) // false }

Правильный шаблон — проверять ошибку до оборачивания:

func addContext(err error) error { if err == nil { return nil } return fmt.Errorf("read config: %w", err) }

Так сохраняется важный контракт Go: nil означает отсутствие ошибки, а ненулевое значение — её наличие. Проверять нужно именно интерфейс error до вызова fmt.Errorf; отдельно следует помнить о типизированном nil, который уже может находиться в ненулевом интерфейсе.

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

Слой конфигурации возвращает ошибку чтения файла, а общий помощник добавляет имя подсистемы. Первый вариант безусловно вызывает fmt.Errorf и для успешного чтения возвращает ненулевую ошибку. Это решение проще визуально, но создаёт ложные сбои и может быть обнаружено только интеграционными тестами.

Второй вариант требует проверку nil в каждом месте вызова. Он корректен, но дублирует логику и повышает риск, что новый вызов забудет проверку.

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

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

  1. Изменится ли результат, если вместо %w использовать %v?

Нет. fmt.Errorf в обоих случаях создаст ненулевую ошибку даже при nil-аргументе. Разница в другом: %w сохраняет исходную ошибку для обхода через errors.Is и errors.As, а %v только форматирует её текст.

  1. Достаточно ли проверить err == nil внутри функции-обёртки?

Для обычного nil этого достаточно. Но значение конкретного указательного типа, равное nil, может быть помещено в интерфейс error; сам интерфейс тогда будет ненулевым. Поэтому функция может корректно увидеть ненулевой error и обернуть его, хотя внутри находится типизированный nil. Это отдельная проблема формирования возвращаемых ошибок: обычно не следует возвращать типизированный nil как error.

  1. Что произойдёт при последующей проверке причины у ошибки, созданной из nil?

Такая ошибка сама по себе ненулевая, но её причина отсутствует. errors.Is не признает её равной nil, а поиск конкретной причины не найдёт исходный объект. Поэтому проверка nil должна выполняться до оборачивания, а не откладываться до обработки результата.