Анализ интерфейса под Golang

Go

Интерфейсы в 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):

图1

Функция convT2Inoptr передает параметры *itab и *xitehip в исходном коде. На рисунке 2 показан прототип типа itab и данные в памяти.Обнаружено, что itab действительно является полем в исходном коде во время выполнения. Всего 32 байта. ([4]uint8 не занимает байтов)

图2
Рисунок 3 — это данные elem, это структурный тип с именем xitehip, в котором хранится age=18. 0x12 в памяти — это точно age=18. Обратите внимание, что адрес в это время: 0xc000032777.
图3
Рисунок 4 представляет собой тип данных переменной xh и данные поля данных в ней. Выяснилось, что xh действительно является типом iface, а адрес xh.data не 0xc000032777, упомянутый выше, а 0xc000014098, что доказывает, что копируется структура типа xitehip.
图4

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:

图5

1.3 Как XH находит метод прогона? Мы продолжаем видеть рисунок 6, а соответствующие объяснения были отмечены на рисунке:

图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:

图7
Непосредственно отследите данные памяти xh.tab.fun и обнаружите, что метод eat действительно находится по адресу 0x44f940. Выше было сказано, что размер массива fun равен всего 1, поэтому метод run должен быть позади eat, но gdb не указывает, какое место является начальной позицией run. Чтобы убедиться, что run отстает от eat, я перешел непосредственно к отладке, чтобы увидеть, где находится адрес входа eat, как показано на рис. 8.
图8
Адрес инструкции запуска — 0x44fa60. Затем я напечатаю конкретное значение, на которое указывает этот адрес, как показано на рисунке 9:
图9
Давайте взглянем на рисунок 7. Чтобы было понятнее, я сделаю еще один скриншот на основе рисунка 7, см. рисунок 10:
图10
Обнаружено, что инструкции методов запуска на рисунке 9 и рисунке 10 одинаковы, что доказывает, что инструкции двух методов действительно расположены вместе.

Таким образом, объект указательного типа вызывает метод получателя неуказательного типа, а компилятор автоматически преобразует получателя в указательный тип, вызывающий объект находит соответствующий список инструкций метода через массив xh.tab.fun.

Тогда xh — это интерфейс типа значения, а приемник метода, реализованного интерфейсом, — типа указателя.Может ли вызывающая сторона вызвать этот метод указателя?Ответ в том, что он не только не может даже скомпилироваться, см. рисунок 11:

图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:

图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

Анализ исходного кода интерфейса Go