Анатомия интерфейсов в Go
В процессе знакомства с Go я нашел в документации пример:
func returnsError() error {
var p *MyError = nil
if bad() {
p = ErrBad
}
return p // Will always return a non-nil error.
}
После его просмотра меня заинтересовало, почему returnsError
всегда будет возвращать не non-nil error?
Переменные в Go всегда инициализируются значением. Это относится и к интерфейсам. Стоит отметить, что интерфейсы реализованы в виде двух элементов: тип (T
) и значение (V
). Это достаточно поверхностное определение, которое чуть дальше мы разберем. Значение интерфейса будет nil
только в том случае, если V
и T
оба будут nil
.
Есть интересный момент, а именно случай, когда V=nil
, а T!=nil
. В таком случае никакие проверки интерфейса на nil
нам не помогут. А ведь именно этот сценарий и происходит в returnsError
.
Мне стало интересно, как именно эти проверки проходят в Go.
type Word struct {
name string
priority uint
}
type Foo interface {
foo()
}
func (w *Word) foo() {
fmt.Println("call foo()")
}
func (w *Word) noFoo() {
fmt.Println("call noFoo()")
}
func call(f Foo) {
if f != nil {
f.foo()
} else {
fmt.Println("f null")
}
}
func main() {
var f1 *Word
call(f1)
}
В приведенном выше коде есть проверки. Все отрабатывает без ошибок. Вывод будет следующим:
call foo()
Кажется несколько странным, ведь передали nil
и вообще ожидалось что либо не пройдет проверки на nil
, либо выстрелит при выполнении.
Давайте разбираться.
Функция call
принимает интерфейс Foo
. Как вообще происходит преобразование указателя Word
к интерфейсу Foo
?
Упрощенно интерфейс выглядит следующим образом:
Интерфейс:
1. статический тип
2. динамическая информация:
— динамический тип
— динамическое значение
В нашем примере статическим типом будет Foo
, динамическим типом Word
, значением nil
.
Посмотрим более подробно.
Языки с методами можно разделить на два вида: создают таблицы виртуальных методов (например C++) или выполняйте поиск метода при каждом вызове (Smalltalk). Во втором случае для оптимизаций используется кеширование. Go находится посередине: у него есть таблица методов, но вычисляется она во время выполнения.
Из чего же состоят интерфейсы в Go?
type iface struct {
tab *itab
data unsafe.Pointer
}
data
— это непосредственно наш f1 из примера. Вся информация по методам, типам и прочему скрывается в tab
— интерфейсной таблицей (interface table).
type itab struct {
inter *interfacetype
_type *_type
hash uint32 // copy of _type.hash. Used for type switches.
_ [4]byte
fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}
Обратите внимание, что itab
соответствует статическому типу интерфейса (Foo
), а не динамическому (Word
). Для большей точности она соответствует паре Foo
-Word
. Поэтому внутри itab
будут только методы, которые удовлетворяют интерфейсу Foo
и noFoo здесь не будет.
inter
— содержит метаданные о статическом типе интерфейса._type
— о динамическом типеfun
— набор указателей на методы интерфейса. является variable sized.
Откуда берется itable
?
itab
— представляет собой своего рода ключ значение, а именно соответствие статического типа динамическому (помните говорил выше проFoo
-Word ?
). В больших программах типов и интерфейсов великое множество. И не все комбинации нужны. Для этого в Go компилятор создают несколько таблиц описания типов. В первой содержится список методов для конкретного интерфейса. Во второй, какие методы содержат динамические типы. Наш itable
— это соответствие между этими двумя таблицами. itable
кэшируется после создания, так что это соответствие нужно вычислить только один раз.
Как же метод f.foo()
будет вызываться и почему нет ошибки?
Будет некоторый аналог:
f.tab.fun[0](f.data)
Мы разобрали, что будет в fun[0]
(единственный метод нашего интерфейса Foo
) и что содержит в data
(сам объект Word
, в примере мы передали указатель на него неинициализированным — nil
).
В некоторых языках (например C++) для методов есть такое понятие как this-call
. Оно означает, что первым скрытым параметром любого метода идет указатель на сам объект (this
). И если нужно изменять/читать поля объекта мы к ним обращаемся неявно через this. При этом есть статические методы, которые можно вызывать без создания объекта — для таких методов this
не передается неявно т.к. не нужен.
В нашем примере как раз это и получается. Метод foo
никак не взаимодействует с Word
. И поэтом f.data
(которая nil
) не используется и программа корректно завершает работу.
Если изменим реализацию метода — то все упадет как надо:
func (w *Word) foo() {
fmt.Printf("call foo(): %s\n", w.name)
}
Ранее было утверждение:
Значение интерфейса будет nil
только в том случае, если V
и T
оба будут nil
. Если теперь говорить про код: значение интерфейса будет nil
только в том случае, если data
и _type
равны nil
одновременно, ведь вариант когда только data
равно nil
, вполне корректен. Нулевое значение интерфейса, которое не содержит значения как такового, не совпадает со значением интерфейса, содержащим нулевой указатель, в чем мы убедились, посмотрев структуры.
Спасибо за внимание.