Практическая ситуация: функция возвращает интерфейс, внутри которого оказался nil-указатель. Как объяснить, почему сравнение результата с nil даёт false?
Сравнение даёт false, потому что интерфейсное значение состоит из динамического типа и динамического значения. Интерфейс, содержащий указатель типа *T со значением nil, сам не является nil: его динамический тип известен, хотя динамическое значение пусто.
Интерфейсы Go создавались как способ описывать поведение через набор методов, не связывая код с конкретной реализацией. Для этого интерфейс хранит не только значение, но и информацию о его динамическом типе.
Такое устройство позволяет передавать в один интерфейс значения разных типов. Обратная сторона — различие между полностью nil-интерфейсом и интерфейсом, содержащим typed nil, то есть nil-значение конкретного ссылочного типа.
Полностью nil-интерфейс не содержит ни динамического типа, ни динамического значения. Интерфейс с nil-указателем содержит динамический тип, например *T, поэтому он не равен nil.
Из-за этого функция может выглядеть успешно выполнившейся, но вызывающий код не обнаружит отсутствие объекта обычной проверкой интерфейса на nil. Ошибка часто проявляется при возврате ошибок, обработчиков или реализаций интерфейсов, представленных указателями.
Упрощённо интерфейс можно рассматривать как пару: динамический тип и динамическое значение. Значение интерфейса равно nil только тогда, когда отсутствуют оба компонента.
В примере v хранит динамический тип *Item и nil-указатель. Поэтому проверка v == nil возвращает false, а успешное утверждение типа извлекает nil-указатель.
Для интерфейса без динамического типа утверждение типа неуспешно: безопасная форма с двумя результатами вернёт ok == false, а однорезультатная форма вызовет панику. После успешного утверждения вызов метода также может привести к панике, если метод не поддерживает nil-получатель.
Надёжнее не возвращать typed nil как интерфейс. Обычно возвращают сам указатель с согласованной проверкой, используют значение-реализацию вместо указателя, либо явно возвращают признак отсутствия объекта. Для ошибок особенно важно формировать и возвращать именно nil-интерфейс при отсутствии ошибки, а не интерфейс, содержащий nil-указатель.
Проверка через reflection возможна, но это крайняя мера: она усложняет код и требует учитывать, что операция проверки на nil допустима только для nil-able типов, например указателей, срезов, карт, функций, каналов и интерфейсов.
Сервис возвращал интерфейс ошибки. Конкретная ошибка была указателем на структуру, и при отсутствии ошибки функция возвращала этот указатель внутри интерфейса. На уровне вызывающего кода проверка результата на nil не срабатывала, поэтому сервис сообщал об ошибке даже при успешной операции.
Рассматривались три варианта:
Выбрали третий вариант. В результате проверка ошибки стала обычной и предсказуемой, а создание конкретной ошибки осталось только на ветке, где она действительно нужна.
Однорезультатная форма утверждения типа завершится паникой, потому что у интерфейса нет динамического типа, который можно проверить. Безопасная форма с двумя результатами вернёт нулевое значение целевого типа и false. Это отличается от интерфейса с typed nil: там утверждение типа может быть успешным, а извлечённый указатель будет nil.
Да, если динамический тип реализует интерфейс и метод допускает nil-получатель. Метод получит nil-указатель, поэтому результат зависит от его реализации: он может корректно вернуть значение или завершиться паникой при разыменовании указателя. Сам факт успешного вызова метода не означает, что объект существует.
Generics сохраняют информацию о параметре типа внутри конкретного экземпляра функции, но при передаче значения в интерфейс правила интерфейсного представления остаются прежними. Указатель, равный nil, всё равно может быть упакован в интерфейс вместе с динамическим типом, после чего сравнение интерфейса с nil даст false. Поэтому контракт функции должен отдельно определять, как обозначается отсутствие значения.