Интерфейсы в golang делятся на интерфейсы с методами и пустые интерфейсы. Интерфейс с методами представлен iface на нижнем уровне, а нижний слой пустого интерфейса представлен eface. Давайте посмотрим на принципы работы этих двух типов интерфейсов через нижний уровень.
Вот прототип интерфейса:
//runtime/runtime2.go
//非空接口
type iface struct {
tab *itab
data unsafe.Pointer
}
type itab struct {
inter *interfacetype
_type *_type
link *itab
hash uint32 // copy of _type.hash. Used for type switches.
bad bool // type does not implement interface
inhash bool // has this itab been added to hash?
unused [2]byte
fun [1]uintptr // variable sized
}
//******************************
//空接口
type eface struct {
_type *_type
data unsafe.Pointer
}
//========================
//这两个接口共同的字段_type
//========================
//runtime/type.go
type _type struct {
size uintptr
ptrdata uintptr // size of memory prefix holding all pointers
hash uint32
tflag tflag
align uint8
fieldalign uint8
kind uint8
alg *typeAlg
// gcdata stores the GC type data for the garbage collector.
// If the KindGCProg bit is set in kind, gcdata is a GC program.
// Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
gcdata *byte
str nameOff
ptrToThis typeOff
}
//_type这个结构体是golang定义数据类型要用的,讲到反射文章的时候在具体讲解这个_type。
1.iface
1.1 Как типы переменных преобразуются в типы интерфейсов?
См. код ниже:
package main
type Person interface {
run()
}
type xitehip struct {
age uint8
}
func (o xitehip)run() {
}
func main() {
var xh Person = xitehip{age:18}
xh.run()
}
Переменная xh представляет собой тип интерфейса Person.Как тип структуры xitehip преобразуется в тип интерфейса? Взгляните на сгенерированный ассемблерный код:
0x001d 00029 (main.go:13) PCDATA $2, $0
0x001d 00029 (main.go:13) PCDATA $0, $0
0x001d 00029 (main.go:13) MOVB $0, ""..autotmp_1+39(SP)
0x0022 00034 (main.go:13) MOVB $18, ""..autotmp_1+39(SP)
0x0027 00039 (main.go:13) PCDATA $2, $1
0x0027 00039 (main.go:13) LEAQ go.itab."".xitehip,"".Person(SB), AX
0x002e 00046 (main.go:13) PCDATA $2, $0
0x002e 00046 (main.go:13) MOVQ AX, (SP)
0x0032 00050 (main.go:13) PCDATA $2, $1
0x0032 00050 (main.go:13) LEAQ ""..autotmp_1+39(SP), AX
0x0037 00055 (main.go:13) PCDATA $2, $0
0x0037 00055 (main.go:13) MOVQ AX, 8(SP)
0x003c 00060 (main.go:13) CALL runtime.convT2Inoptr(SB)
0x0041 00065 (main.go:13) MOVQ 16(SP), AX
0x0046 00070 (main.go:13) PCDATA $2, $2
0x0046 00070 (main.go:13) MOVQ 24(SP), CX
Есть функция преобразования, найденная из сборки: Runtime.convt2inoPTR (SB) Давайте посмотрим на реализацию этой функции:
func convT2Inoptr(tab *itab, elem unsafe.Pointer) (i iface) {
t := tab._type
if raceenabled {
raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2Inoptr))
}
if msanenabled {
msanread(elem, t.size)
}
x := mallocgc(t.size, t, false)//为elem申请内存
memmove(x, elem, t.size)//将elem所指向的数据赋值到新的内存中
i.tab = tab //设置iface的tab
i.data = x //设置iface的data
return
}
Из приведенной выше реализации мы обнаруживаем, что исходные данные структуры, сгенерированные компилятором, будут скопированы, а затем новый адрес данных будет присвоен iface.data для создания полного iface, так что xh в следующем исходном коде преобразуется в тип интерфейса Person.
var xh Person = xitehip{age:18}
Взгляните на реальную работу с gdb (см. рис. 1):
Функция convT2Inoptr передает параметры *itab и *xitehip в исходном коде. На рисунке 2 показан прототип типа itab и данные в памяти.Обнаружено, что itab действительно является полем в исходном коде во время выполнения. Всего 32 байта. ([4]uint8 не занимает байтов)
Рисунок 3 — это данные elem, это структурный тип с именем xitehip, в котором хранится age=18. 0x12 в памяти — это точно age=18. Обратите внимание, что адрес в это время: 0xc000032777.Рисунок 4 представляет собой тип данных переменной xh и данные поля данных в ней. Выяснилось, что xh действительно является типом iface, а адрес xh.data не 0xc000032777, упомянутый выше, а 0xc000014098, что доказывает, что копируется структура типа xitehip.1.2 Как тип переменной указателя преобразуется в тип интерфейса?
Или вышеприведенный пример просто преобразует
var xh Person = xitehip{age:18}
заменяется
var xh Person = &xitehip{age:18}
Как переменная типа указателя преобразуется в тип интерфейса? См. ассемблерный код ниже:
0x001d 00029 (main.go:13) PCDATA $2, $1
0x001d 00029 (main.go:13) PCDATA $0, $0
0x001d 00029 (main.go:13) LEAQ type."".xitehip(SB), AX
0x0024 00036 (main.go:13) PCDATA $2, $0
0x0024 00036 (main.go:13) MOVQ AX, (SP)
0x0028 00040 (main.go:13) CALL runtime.newobject(SB)
0x002d 00045 (main.go:13) PCDATA $2, $1
0x002d 00045 (main.go:13) MOVQ 8(SP), AX
0x0032 00050 (main.go:13) MOVB $18, (AX)
Нашел эту функцию:
runtime.newobject(SB)
Давайте посмотрим на конкретную реализацию:
// implementation of new builtin
// compiler (both frontend and SSA backend) knows the signature
// of this function
func newobject(typ *_type) unsafe.Pointer {
return mallocgc(typ.size, typ, true)
}
Компилятор автоматически генерирует iface и назначает адрес объекта, созданного &xitehip{age:18} (через newobject), в iface.data. То есть структура xitehip не копируется. Взгляните на рисунок 5 с gdb:
1.3 Как XH находит метод прогона? Мы продолжаем видеть рисунок 6, а соответствующие объяснения были отмечены на рисунке:
1.4 Правила вызова интерфейса
Добавьте к приведенному выше примеру метод интерфейса EAT() и реализуйте его (обратите внимание, что получателем этого метода интерфейса является указатель).
package main
type Person interface {
run()
eat(string)
}
type xitehip struct {
age uint8
}
func (o xitehip)run() { // //接收方o是值
}
func (o *xitehip)eat(food string) { //接收方o是指针
}
func main() {
var xh Person = &xitehip{age:18} //xh是指针
xh.eat("ma la xiao long xia!")
xh.run()
}
Фактическим типом переменной xh в этом примере является указатель, так как же он вызывает метод run без указателя? Продолжайте отслеживать с помощью gdb, как показано на рисунке 7:
Непосредственно отследите данные памяти xh.tab.fun и обнаружите, что метод eat действительно находится по адресу 0x44f940. Выше было сказано, что размер массива fun равен всего 1, поэтому метод run должен быть позади eat, но gdb не указывает, какое место является начальной позицией run. Чтобы убедиться, что run отстает от eat, я перешел непосредственно к отладке, чтобы увидеть, где находится адрес входа eat, как показано на рис. 8.Адрес инструкции запуска — 0x44fa60. Затем я напечатаю конкретное значение, на которое указывает этот адрес, как показано на рисунке 9:Давайте взглянем на рисунок 7. Чтобы было понятнее, я сделаю еще один скриншот на основе рисунка 7, см. рисунок 10:Обнаружено, что инструкции методов запуска на рисунке 9 и рисунке 10 одинаковы, что доказывает, что инструкции двух методов действительно расположены вместе.Таким образом, объект указательного типа вызывает метод получателя неуказательного типа, а компилятор автоматически преобразует получателя в указательный тип, вызывающий объект находит соответствующий список инструкций метода через массив xh.tab.fun.
Тогда xh — это интерфейс типа значения, а приемник метода, реализованного интерфейсом, — типа указателя.Может ли вызывающая сторона вызвать этот метод указателя?Ответ в том, что он не только не может даже скомпилироваться, см. рисунок 11:
Сводную информацию см. в таблице ниже:абонент | получатель | Может ли он быть скомпилирован |
---|---|---|
ценность | ценность | true |
ценность | указатель | false |
указатель | ценность | true |
указатель | указатель | true |
указатель | указатели и значения | true |
ценность | указатели и значения | false |
Из вышеприведенной таблицы можно сделать следующие выводы:
Когда вызывающий объект является значением, компилятор не разрешает компиляцию, пока получатель имеет метод указателя.
2 eface
Пустой интерфейс не имеет списка методов относительно непустого интерфейса.
type eface struct {
_type *_type
data unsafe.Pointer
}
Первый атрибут изменен с itab на _type, эта структура является основой типов переменных в golang, поэтому пустой интерфейс может указывать любой тип переменной.
2.1 Пример:
cpackage main
import "fmt"
type xitehip struct {
}
func main() {
var a interface{} = xitehip{}
var b interface{} = &xitehip{}
fmt.Println(a)
fmt.Println(b)
}
GDB спросил на рисунке 12:
2.2 Утверждения
Определить тип данных переменной
s, ok := i.(TypeName)
if ok {
fmt.Println(s)
}
Если нет ok, неправильный тип вызовет панику.
Вы также можете использовать форму переключения:
switch v := v.(type) {
case TypeName:
...
}
3 Проверьте интерфейс
3.1 Используйте компилятор для проверки реализации интерфейса
var _ InterfaceName = (*TypeName)(nil)
3.2 нулевой и нулевой интерфейс
3.2.1 nil
func main() {
var i interface{}
if i == nil {
println(“The interface is nil.“)
}
}
(gdb) info locals;
i = {_type = 0x0, data = 0x0}
3.2.2 Если внутреннее значение данных интерфейса равно нулю, но вкладка не пуста, интерфейс является нулевым интерфейсом.
// go:noinline
func main() {
var o *int = nil
var i interface{} = o
if i == nil {
println("Nil")
}
println(i)
}
(gdb) info locals;
i = {_type = 0x21432f8 <type.*+36723>, data = 0x0}
o = 0x0
3.2.3 Проверка с отражением
v := reflect.ValueOf(a)
if v.Isvalid() {
println(v.IsNil()) // true, This is nil interface
}
Ссылаться на
Анализ реализации интерфейса Go — облачная технология Xiaomi
10 вопросов об интерфейсе в углубленной расшифровке языка Go