Давайте воспользуемся небезопасными пакетами stdlib, чтобы увидеть внутренности значений интерфейса.

Когда мы начали работать с интерфейсами Go, мы быстро узнали, что значение интерфейса содержит копию назначенного ему значения. Это согласуется с принципом, согласно которому все в Go «передается как значение».

Рассмотрим простой случай. У нас есть значение типа MyType, хранящееся в переменной myTypeValue, которая присваивается переменной aVar типа interface{}.

Тот факт, что копия передается интерфейсу, означает, что в игре есть два значения типа MyType в двух разных местах памяти.

Можем ли мы доказать это, взглянув на адреса памяти этих двух значений, одного, хранящегося в переменной myTypeValue, и другого, заключенного в значение интерфейса, хранящееся в переменной aVar?

Нет мы не можем. Конечно, мы можем получить адрес myTypeValue. Но мы не можем получить адрес значения, заключенного в значение интерфейса, поскольку «Конкретное значение, хранящееся в интерфейсе, не адресуется, так же как и элемент карты не адресуется.»

Для этого есть веские причины, и нам на самом деле не стоит даже думать о том, чтобы делать что-то подобное. Но что, если мы упорно хотим посмотреть, действительно ли адреса разные? Это не потому, что мы не доверяем официальной документации; это просто для того, чтобы повеселиться и немного изучить небезопасные склоны языка.

Небезопасная поездка

Любое значение интерфейса представляет собой структуру данных из двух слов. Одно слово содержит указатель на конкретное значение, а другое — указатель на дескриптор типа конкретного значения.

Теперь предположим, что мы создали пользовательский тип структуры, определяющий структуру данных из двух слов, такую ​​же, как у интерфейса. Это может быть что-то вроде типа eface, который выглядит так:

// eface is a type struct that has the same memory layout as any interface value
type eface struct {
 typ, val unsafe.Pointer // unsafe.Pointer size is 1 word
}

eface — это обычный тип структуры, и поэтому он адресуемый. Другими словами, мы можем получить адреса полей, содержащихся в eface значениях. В то же время у него такая же схема памяти значения типа interface{}. Следовательно, если бы мы могли наложить значение типа eface на ту же память, которая используется значением interface{} aVar, у нас был бы доступ к искомому адресу, адресу конкретного значения, обернутого интерфейсом.

Правила языка не позволяют нам «приводить» значение типа interface{} к другому типу, например eface, поэтому нужно придумывать что-то еще. В этом случае нам на помощь приходит пакет unsafe.

Используя пакет unsafe, мы можем создать функцию, которая получает значение interface{} в качестве входных данных, берет его указатель и «приводит» его к значению типа *eface.

func toEface(arg interface{}) *eface {
 return (*eface)(unsafe.Pointer(&arg))
}

Это небезопасная игра, но мы сказали это в начале: это небезопасное путешествие, которое мы совершаем просто для развлечения.

Теперь, когда у нас есть указатель на значение eface, мы можем извлечь поле val, содержащее указатель на значение, завернутое в интерфейс.

func ifaceConcreteValPtr(arg interface{}) unsafe.Pointer {
 ptrToEfaceVal := toEface(arg)
 return ptrToEfaceVal.val
}

В конце концов, у нас есть указатель на конкретное значение, обернутое внутри значения интерфейса. Поэтому мы можем проверить, отличается ли это от указателя на значение, хранящееся в myTypeValue, которое является исходным значением, которое мы присвоили интерфейсу.

Угадай, что? Два адреса на самом деле разные. Спецификации языка не лгут: интерфейс упаковывает копию присвоенного ему значения.

Но почему это не адресно?

Теперь, когда мы завершили наше небезопасное путешествие, возможно, стоит обсудить, почему Go делает значения интерфейса не адресуемыми. Причина в том, чтобы защитить безопасность типов, даже когда мы имеем дело со значениями интерфейса, которые добавляют языку своего рода «динамику типов».

Рассмотрим следующий код:

var aVar interface{} = "Abc"
// valid operation: assign a copy of the string held by the interface value to 
// the variable aString
aString := aVar.(string)   

// compiler error
// invalid operation: cannot take address of aVar.(string)
aPtr := &aVar.(string)

Если бы последняя операция была разрешена, переменная aPtr имела бы тип *string (указатель на строку) и, следовательно, что-то вроде этого len(*aPtr) было бы разрешено.

Но если мы затем присвоим aVar новое значение другого типа, каким будет тип aPtr?

var aVar interface{} = "Abc"  

// let's assume this were a valid operation
aPtr := &aVar.(string)

// we assign a new value of a different type to aVar
aVar = 100

aPtr явно должен иметь тип *int, так как теперь aVar оборачивает значение типа int. Но это будет означать, что тип aPtr изменяется во время выполнения программы, что противоречит основному правилу статически типизированного языка, такого как Go.

Почему возникает ошибка «у метода нет получателя указателя»

Кстати, тот факт, что значение интерфейса не адресуется, является основной причиной ужасной ошибки:

‹type› не реализует ‹interface› (метод ‹name› имеет получатель указателя)

Причина проста. Если у нас есть значение, обернутое внутри значения интерфейса, и обернутое значение не адресуется, мы не можем получить указатель на него. Если мы не можем получить указатель, мы не можем вызывать методы с получателем указателя. Если мы попытаемся, компилятор сообщит нам: «метод имеет получатель указателя», и мы не сможем получить указатель.

Заключительный отказ от ответственности

Мы начали с любопытства. Мы хотели проверить с помощью адресов памяти, что значение, помещенное в интерфейс, является копией исходного значения. Чтобы достичь нашей цели, мы использовали пакет unsafe, чтобы копаться во внутренностях языка.

В нашей поездке мы немного изучили внутренности интерфейсов Go, но помните, что пакет unsafe называется небезопасным, потому что он небезопасен. Более того, использование пакета unsafe не обязательно совместимо с будущими версиями языка. Наконец, внутренняя структура значений интерфейса не документирована и может меняться со временем.

Таким образом, этот код не следует соблюдать. Это просто для любопытства и развлечения.

Полный пример можно найти в этом репо.

Благодарности

Этот пост был вдохновлен этим ответом на переполнение стека.

Последняя часть, связанная с безопасностью типов, была вдохновлена ​​этим другим ответом на переполнение стека.