Как правильно проверять на nil в Go: интерфейсы, структуры и указатели
Почему ваш код не работает: интерфейс ≠ указатель
Вы столкнулись с классической ловушкой Go. Проблема в том, что интерфейс в Go — это структура из двух полей: указатель на тип (type) и указатель на значение (value). Когда вы присваиваете *MyError(nil) переменной типа error, интерфейс получает:
- type =
*MyError - value =
nil
Интерфейс считается nil только когда оба поля равны nil. В вашем случае type не nil (он равен *MyError), поэтому err != nil возвращает true, хотя значение внутри — nil.
Как это исправить: три основных подхода
1. Возвращайте конкретный nil, а не типизированный
func doSomething() error {
// ... логика, не приведшая к ошибке
return nil // это настоящий nil интерфейса
}
Если нужно вернуть ошибку — создавайте её явно:
func doSomething() error {
if someCondition {
return &MyError{Code: 42, Msg: "что-то пошло не так"}
}
return nil
}
2. Используйте тип error для всех возвращаемых значений
Не объявляйте переменные конкретного типа ошибки:
func doSomething() error {
var err error = nil // явно интерфейс
// ... логика
return err
}
3. Проверяйте через рефлексию (не рекомендуется для production)
import "reflect"
func isNilInterface(i interface{}) bool {
return i == nil || reflect.ValueOf(i).IsNil()
}
Правильный паттерн для пользовательских ошибок
// Определяем ошибку как значение, а не указатель
type MyError struct {
Code int
Msg string
}
func (e MyError) Error() string { // метод на значении, а не на указателе
return fmt.Sprintf("error %d: %s", e.Code, e.Msg)
}
func doSomething() error {
// Теперь можно вернуть nil напрямую
return nil
}
// Если нужно вернуть ошибку с данными:
func doSomethingElse() error {
return MyError{Code: 404, Msg: "not found"}
}
Как отлаживать такие ситуации
Используйте fmt.Printf("%T %v\n", err, err) — вы увидите тип и значение. Если тип не nil, а значение nil — это "дырявый" интерфейс.
Золотое правило: всегда возвращайте nil через return nil, а не через переменную конкретного типа. Если используете пользовательские типы ошибок — работайте с ними через значения, а не указатели, или всегда проверяйте err != nil только на уровне интерфейса error.
Альтернативный подход: используйте паттерн "nil-безопасные структуры"
Вместо того чтобы бороться с типизированным nil, можно проектировать код так, чтобы nil никогда не попадал в интерфейс. Один из способов — использовать функциональные опции или nil-объект (Null Object Pattern).
// Nil-объект для ошибок
type noError struct{}
func (noError) Error() string { return "" }
var NoError error = noError{}
func doSomething() error {
// ... логика
if everythingOK {
return NoError // не nil, но ведёт себя как отсутствие ошибки
}
return &MyError{Code: 500, Msg: "fail"}
}
func main() {
err := doSomething()
if err != NoError {
fmt.Println("Реальная ошибка:", err)
}
}
Этот подход удобен, когда вам нужно различать "нет ошибки" и "ошибка есть, но нулевая". Однако он добавляет сложность и не рекомендуется для простых случаев — лучше просто возвращать nil из функций.
Ещё один лайфхак: если вы используете указатели на структуры, всегда проверяйте if err != nil на уровне возвращаемого интерфейса, а не на уровне внутренней переменной. И никогда не присваивайте nil интерфейсной переменной через конкретный тип — это главная причина багов.
Ответов пока нет.
Войдите чтобы оставить ответ