Интерфейс равен nil, только если у него нет динамического типа и значения. При возврате nil-указателя *ReadError как error тип сохраняется. На успешной ветке возвращайте литерал nil.
Воспроизводим ненулевой error с nil внутри
Сохраните программу в main.go и запустите go run main.go. Первая строка — true true false. Указатель pointer равен nil, empty является nil-интерфейсом, но присваивание pointer переменной boxed добавляет динамический тип *main.ReadError.
Вторая строка — *main.ReadError <nil ReadError>, третья — false. Метод Error специально обрабатывает nil receiver, чтобы вывод был безопасным. Напечатанная строка не является проверкой равенства: форматирование может скрывать сведения о типе, от которых зависит сравнение.
package main
import "fmt"
type ReadError struct{ Path string }
func (e *ReadError) Error() string {
if e == nil { return "<nil ReadError>" }
return "cannot read " + e.Path
}
func broken() error {
var issue *ReadError
return issue
}
func main() {
var pointer *ReadError
var empty error
var boxed error = pointer
fmt.Println(pointer == nil, empty == nil, boxed == nil)
fmt.Printf("%T %v\n", boxed, boxed)
fmt.Println(broken() == nil)
}Разделяем интерфейс и содержащееся в нём значение
Динамический тип и значение — удобная модель рассуждения, а не обещание конкретной раскладки памяти runtime. При преобразовании типизированного указателя в интерфейс конкретный тип сохраняется, даже если сам указатель равен nil.
Сравнивать интерфейс непосредственно с nil безопасно. Сравнение двух интерфейсов — другой случай: при одинаковом несравнимом динамическом типе, например slice, оно может вызвать panic. Из правила про nil нельзя делать вывод, что любые сравнения интерфейсов безопасны.
| Объявление | Динамическое содержимое | value == nil |
|---|---|---|
| var pointer *ReadError | Не интерфейс; nil-указатель | true |
| var empty error | Нет динамического типа и значения | true |
| var boxed error = pointer | *ReadError с nil-указателем | false |
| var value any = []int(nil) | []int с nil-срезом | false |
Исправляем контракт успешного результата
Вызывающий код не должен использовать reflection, чтобы понять, завершилась ли операция успешно. Оставьте возвращаемый тип error и явно верните nil на успешной ветке. Конкретную ошибку создавайте только при реальном сбое.
Программа печатает true, затем cannot read config.json. На ошибочной ветке создаётся настоящий объект, поэтому Error больше не нуждается в nil receiver для этого сценария. Добавьте проверку успешного вызова: тест только текста ошибки не обнаружил бы исходную проблему.
package main
import "fmt"
type ReadError struct{ Path string }
func (e *ReadError) Error() string { return "cannot read " + e.Path }
func read(fail bool) error {
if fail { return &ReadError{Path: "config.json"} }
return nil
}
func main() {
fmt.Println(read(false) == nil)
fmt.Println(read(true))
}Всегда ли вызов метода на типизированном nil вызывает panic?
Нет. Метод с указательным receiver может получить nil и явно обработать его, как в первой программе. Если реализация разыменует receiver без проверки, возможна panic. У настоящего nil-интерфейса вообще нет конкретной реализации метода для вызова.
Поэтому красивый вывод из Error не исправляет контракт возвращаемого значения: error продолжает отличаться от nil. Проверка строки на похожий на nil текст тоже неверна — она превращает представление в логику управления и ломается при изменении форматирования.
Что проверить при неожиданном err != nil
Сначала посмотрите конкретный тип через %T и ветки возврата функции. Ищите переменную указательного типа, которую возвращают через интерфейс без явного успешного возврата nil. Затем отдельно проверьте успех и ошибку. Тест должен фиксировать наблюдаемое поведение API, а не внутреннее устройство runtime.
Для тренировки замените error собственным небольшим интерфейсом и предскажите результат до запуска. Такая же проблема встречается в фабриках, возвращающих интерфейсы. Определите контракт: отсутствие реализации и реализация с пустым внутренним состоянием могут означать разные вещи.
- Проследите преобразование типа в return.
- Для отсутствующего значения на границе интерфейса возвращайте nil.
- Поддерживайте nil receiver только как осознанную часть контракта метода.
- errors.Is(err, nil) не превращает типизированный nil в nil-интерфейс и не служит универсальным исправлением.
Коротко
Частые вопросы
Решает ли any проблему типизированного nil?
Нет. any — псевдоним пустого интерфейса. При присваивании типизированного nil-указателя или nil-среза динамический тип сохраняется, и интерфейс не равен nil.
Нужно ли проверять все ошибки через reflection?
Обычно исправлять нужно возвращающую ошибку функцию. Reflection требует проверок kind и при неверном использовании сам может вызвать panic. Он не заменяет ясный контракт успешного результата.
Почему вывод nil не доказывает err == nil?
Форматирование может вызывать методы или показывать вложенное значение. Равенство nil зависит от наличия динамического типа и значения у интерфейса, а не от напечатанного текста.
Источники
Источники и редакционная политика
Сведения проверены 21 октября 2026 г. Ссылки рядом с разделами указывают источники фактов и технических объяснений. Выводы, учебные сценарии и рекомендации по подготовке — редакционная работа RecallDeck.
От чтения к воспроизведению
Отрепетируйте полный цикл интервью.
RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.