Интерфейс и nil-указатель

В Go интерфейс хранит пару значений: динамический тип и динамическое значение. Интерфейс считается nil только тогда, когда оба этих поля равны nil. Если в интерфейс сохранить nil-указатель на конкретный тип, то динамический тип становится известным, и интерфейс перестает быть nil, даже если его значение равно nil.

Это одна из самых частых ловушек в Go, которая приводит к неожиданному поведению при проверке if err == nil, если error — это интерфейс, а возвращаемая ошибка реализована через указатель на структуру.

Синтаксис

Объявление переменной типа интерфейс:

govar i interface{} // nil-интерфейс
var p *int    // nil-указатель
i = p       // i больше не nil

Пример

Рассмотрим базовый пример, демонстрирующий разницу между nil-интерфейсом и интерфейсом, содержащим nil-указатель:

gopackage main

import "fmt"

func main() {
  var i interface{} // nil-интерфейс
  fmt.Println(i == nil)

  var p *int // nil-указатель
  i = p
  fmt.Println(i == nil)
}

Результат выполнения кода:

gotrue
false

Пример

Теперь рассмотрим более жизненный пример с интерфейсом error. Создадим свою структуру ошибки и посмотрим, как поведет себя проверка на nil:

gopackage main

import "fmt"

type MyError struct {
  msg string
}

func (e *MyError) Error() string {
  return e.msg
}

func getError() error {
  var e *MyError = nil
  return e
}

func main() {
  err := getError()
  if err == nil {
    fmt.Println("err is nil")
  } else {
    fmt.Println("err is not nil")
  }
}

Результат выполнения кода:

go"err is not nil"

Несмотря на то, что мы вернули nil-указатель, функция getError возвращает интерфейс error, в котором сохранен тип *MyError и значение nil. Поэтому интерфейс не равен nil.

Пример

Правильный способ обработки таких ситуаций — возвращать nil только в том случае, если ошибки действительно нет:

gopackage main

import "fmt"

type MyError struct {
  msg string
}

func (e *MyError) Error() string {
  return e.msg
}

func getError(flag bool) error {
  if flag {
    return &MyError{msg: "something went wrong"}
  }
  return nil // возвращаем именно nil, а не nil-указатель
}

func main() {
  err := getError(false)
  if err == nil {
    fmt.Println("err is nil")
  } else {
    fmt.Println("err is not nil")
  }
}

Результат выполнения кода:

go"err is nil"

Смотрите также

  • понятие nil-интерфейса,
    который не содержит ни типа, ни значения
  • механизм type assertion,
    позволяющий извлечь динамическое значение из интерфейса
  • конструкцию type switch,
    которая выполняет действия в зависимости от динамического типа
  • интерфейс error,
    который является стандартным интерфейсом для представления ошибок