В чём разница между nil-интерфейсом и интерфейсом, содержащим nil-указатель?

Ответ

Это классический вопрос-ловушка в Go, который проверяет понимание внутреннего устройства интерфейсов.

Внутренне интерфейс в Go можно представить как кортеж (пару) из двух полей: (тип, значение)

  1. Nil-интерфейс (nil interface)

    Интерфейс равен nil только в том случае, если и тип, и значение в его внутренней паре равны nil.

    var i interface{}
    // Внутреннее представление i: (тип=nil, значение=nil)
    fmt.Println(i == nil) // Выведет: true
  2. Интерфейс, содержащий nil-указатель

    Это интерфейс, который не равен nil, но его динамическое значение — это указатель, который равен nil.

    var p *int = nil
    var i interface{} = p
    // Внутреннее представление i: (тип=*int, значение=nil)

    Здесь поле тип не равно nil (оно равно *int), поэтому и сам интерфейс i не равен nil.

Практический пример и "ловушка"

Это часто приводит к ошибкам при возврате ошибок из функций.

// Определим свой тип ошибки
type MyError struct{}
func (e *MyError) Error() string { return "my error" }

// Эта функция может вернуть nil-указатель типа *MyError
func doSomething() *MyError {
    // ... какая-то логика
    return nil // Возвращаем nil-указатель, а не nil-интерфейс
}

func main() {
    // Тип переменной err - это интерфейс error
    var err error 
    err = doSomething() // doSomething() вернула (*MyError)(nil)

    // Внутреннее представление err: (тип=*MyError, значение=nil)

    if err != nil { 
        // Этот блок выполнится, что может быть неожиданно!
        fmt.Printf("Ошибка! Тип: %T, Значение: %vn", err, err)
        // Вывод: Ошибка! Тип: *main.MyError, Значение: <nil>
    }
}

Вывод:

  • Nil-интерфейс: (type=nil, value=nil). Он == nil.
  • Не-nil интерфейс: (type=T, value=V). Он != nil, даже если V является nil-указателем.