См. ответ Колминатора , но для довольно несовершенной аналогии с прямым C-кодом представьте, что:
var x interface{ ... } // fill in the `...` part with functions
- или в этом случае объявив i I
, чтобы сделать i
имеет тип интерфейса, который вы определили - это все равно что объявить C struct
с двумя членами, один для хранения типа и один для хранения значения этого типа:
struct I {
struct type_info *type;
union {
void *p;
int i;
double d;
// add more types if/as needed here
} u;
};
struct I i;
Компилятор заполняетслот i.type
при передаче &s
на i
и заполнении i.u.p
, чтобы указать на объект s
. 1
Когда вы вызываете i.Set(10)
компилятор Go превращает это в эквивалент:
(*__lookup_func(i, "Set"))(i.u.p)
, где __lookup_func
находит фактическое func (s *S) Set(age int)
, и чрезмерное количество магии обнаруживает, что он должен передать указатель на s
(из i.u.p
) к этой функции-установщику. 2
Тот факт, что переменная некоторых типов интерфейса имеет эти два слота - часть «тип» и часть типа объединения, которая содержиттекущая стоимость - это настоящий секретный соус здесь. Вы можете использовать утверждение типа:
v, ok := i.(int)
или переключатель типа:
switch v := i.(type) {
case int: // code where `v` is `var v int`
case float64: // code where `v` is `var v float64` ...
// add more cases as desired
}
, чтобы проверить слот типа при копировании слота значения в новую переменную v
. 3
Обратите внимание, что переменная interface
сравнивается равной nil
тогда и только тогда, когда оба слота (i.type
иi.u
) - ноль. То, что постоянно запутывает людей, заключается в том, что если вы инициализируете значение interface
из какого-либо неинтерфейсного типа, его слот type
больше не равен нулю, и тест:
if i == nil { // handle the case ...
не делаетне работает, , даже если значение слота (i.u.p
в нашей аналогии здесь) равно nil
.
1 Я показываю это как объединение нескольких типов Си, но не включаю struct
типов. Фактически, размер второго слота значения interface
не является чем-то, что компилятор дает какие-либо обещания, хотя в современных компиляторах это всего лишь 8 байтов, как и любой другой указатель. Если какой-либо тип значения, который у вас есть, слишком велик для реальной базовой реализации, тем не менее, компилятор вставляет выделение: значение помещается в некоторую дополнительную память, и поле указателя объединения устанавливается для указания значения.
во время компиляции компилятор проверяет, соответствует ли тип фактического значения, которое вы вставляете в какой-либо интерфейс, этому интерфейсу. Тип интерфейса имеет список функций, которые он должен поддерживать. Если базовый тип имеет эти функции, присваивание в порядке (и компилятор знает, как построить соответствующие vtable-подобные данные, упомянутые в сноске 2). Если в базовом типе отсутствуют некоторые функции, вы получите ошибку во время компиляции. Таким образом, вы определенно гарантированы, что последующий поиск функции в переменной интерфейса всегда будет успешным.
2 Поиск выполняется быстрее, чем подразумеваемый поиск строки, поскольку Set
имеет целое числокодовое значение, которое компилятор назначил во время компиляции этому конкретному типу интерфейса, и внутреннее содержимое struct type_info
имеет различные таблицы быстрого поиска, несколько сродни таблицам C ++, чтобы помочь ему.
"чрезмерный"количество магии "в большинстве случаев значительно уменьшается, чтобы просто" поместить правильный параметр в правильный регистр аргументов или расположение в стеке ": копировать лишние байты, которые вызываемый вызывающий никогда не считывает, безопасно. Однако если для целочисленного или плавающего числа требуются разные регистры аргументов, это становится немного сложнее, и я не уверен, что на самом деле делают текущие компиляторы Go.
3 В v, ok := i.(int)
форма, если слот типа не удерживает int
, v
установлен на ноль, а ok
установлен на false
. Это справедливо независимо от фактического типа: все типы имеют нулевое значение по умолчанию, а v
становится нулевым значением указанного вами типа.