Разберите, какое значение напечатает программа и почему результат зависит от типа значения внутри интерфейса.
package main
import "fmt"
type MyError struct{}
func (*MyError) Error() string { return "ошибка" }
func makeErr() error {
var e *MyError
return e
}
func main() {
fmt.Println(makeErr() == nil)
}
Программа напечатает false. Переменная e содержит нулевой указатель, но при возврате преобразуется в интерфейс error, который сохраняет динамический тип *MyError; такой интерфейс не равен nil.
Интерфейс равен nil только тогда, когда у него одновременно отсутствуют и динамический тип, и динамическое значение.
Интерфейсы Go предназначены для отделения кода от конкретных реализаций: функция может принимать любое значение, реализующее нужный набор методов. Для этого интерфейсное значение должно хранить не только данные, но и информацию о динамическом типе.
Такой подход решает задачу полиморфизма без наследования и явного объявления намерения реализовать интерфейс. Обратная сторона — нулевое значение указателя и нулевое значение интерфейса являются разными состояниями.
В функции makeErr переменная e имеет тип *MyError и равна nil. При возврате она неявно преобразуется к типу error.
Если разработчик проверяет результат как if err != nil, он получит true, хотя конкретный указатель внутри интерфейса равен nil. Это может привести к ложной обработке ошибки, записи ошибочного события в лог или неожиданному вызову метода на нулевом указателе.
Интерфейсное значение концептуально состоит из двух частей: динамического типа и динамического значения. В данном примере после возврата оно содержит тип *MyError и значение nil.
Сравнение интерфейса с nil проверяет отсутствие обеих частей. Поскольку динамический тип *MyError присутствует, интерфейс не является нулевым.
Безопасный вариант — возвращать непосредственно nil, если ошибки нет:
Однако это не исправляет случай, когда typed nil уже передан через интерфейс. Для диагностики конкретного значения можно использовать type assertion или reflect, но обычно лучше устранить typed nil на границе, где формируется результат.
Если метод интерфейса вызывается через typed nil, результат зависит от реализации метода. Метод может корректно обработать нулевой receiver, но обращение к его полям обычно приведёт к панике. Поэтому проверка err != nil не гарантирует, что внутреннее конкретное значение безопасно для использования.
Обработчик строит ошибку через указатель на структурный тип:
Один вариант — вернуть e напрямую. Его плюс — простой код, но при отсутствии ошибки он возвращает typed nil, поэтому вызывающий код ошибочно считает валидацию неуспешной.
Другой вариант — явно вернуть nil в успешной ветке и указатель только при ошибке. Это выбранное решение: оно сохраняет понятную семантику error и делает проверку if err != nil корректной.
Альтернатива — заменить указательную ошибку значением-структурой, если тип допускает копирование и не требует состояния указателя. Это может убрать проблему typed nil, но не всегда подходит для больших структур, ошибок с изменяемым состоянием или методов, которым нужен указатель receiver.
nil?Да. Это как раз случай var p *MyError; var err error = p. У интерфейса есть динамический тип *MyError, поэтому он не равен nil, хотя значение этого типа — нулевой указатель.
errors.Is или errors.As?Поведение определяется переданными значениями и реализацией методов ошибки. errors.As может успешно найти целевой тип и записать в него нулевой указатель, если цепочка ошибок содержит значение соответствующего динамического типа. Поэтому сам факт успешного errors.As не означает, что полученный указатель ненулевой; после assertion нужно учитывать это отдельно.
error не устраняет проблему автоматически?Именованный результат лишь создаёт переменную интерфейсного типа с нулевым значением nil. Если затем присвоить ему typed nil, например result = (*MyError)(nil), интерфейс снова получит динамический тип *MyError и перестанет быть nil. Именованный результат безопасен только до такого присваивания или при явном return nil.