golang中interface底層分析

golang中的接口分爲帶方法的接口和空接口。 帶方法的接口在底層用iface表示,空接口的底層則是eface表示。下面咱們透過底層分別看一下這兩種類型的接口原理。html

如下是接口的原型:golang

//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的struct類型是如何轉換成接口類型的呢? 看一下生成的彙編代碼:bash

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) 咱們去看一下這個函數的實現:frontend

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
}
複製代碼

從以上實現咱們發現編譯器生成的struct原始數據會複製一份,而後將新的數據地址賦值給iface.data從而生成了完整的iface,這樣以下原始代碼中的xh就轉換成了Person接口類型。函數

var xh Person = xitehip{age:18}
複製代碼

用gdb實際運行看一下(見圖1): ui

圖1

convT2Inoptr函數傳進來的參數是*itab和源碼中的 *xitehip。 圖2是itab的類型原型和內存中的數據發現itab確實是runtime中源碼裏的字段。總共佔了32個字節。([4]uint8 不佔字節) this

圖2
圖3是elem的數據他是個名爲xitehip的結構體類型裏面存放的是age=18。 內存中的0x12正好是age=18。注意此時的地址是:0xc000032777。
圖3
圖4是xh變量的數據類型和其中data字段的數據。發現xh確實是iface類型了且xh.data的地址不是上面提到的0xc000032777 而是0xc000014098,證實是複製了一份xitehip類型的struct。
圖4

1.2 指針變量類型是如何轉換成接口類型的呢?

仍是上面的例子只是將spa

var xh Person = xitehip{age:18}
複製代碼

換成了debug

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這個結構體沒有被複制。 用gdb看一下見圖5:

圖5

1.3 那xh是如何找到run方法的呢?咱們繼續看見圖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的後面,我直接往下debug看eat的入口地址在哪裏,見圖8。
圖8
run指令的地址是0x44fa60。那我去打印一下這個地址所指向的具體的值是什麼,見圖9:
圖9
咱們在看一下圖7中,爲了更清楚我基於圖7再截一次圖,見圖10:
圖10
發現圖9和和圖10的的run方法的指令是同樣的,證實兩個方法的指令確實一塊兒排列的。

總結,指針類型的對象調用非指針類型的接收方的方法,編譯器自動將接收方轉換爲指針類型;調用方經過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的話類型不正確的話會引發panic。

也能夠用switch形式:

switch v := v.(type) {
      case TypeName:
    ...
    }
複製代碼

3 檢查接口

3.1 利用編譯器檢查接口實現

var _ InterfaceName = (*TypeName)(nil)

3.2 nil和nil interface

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 若是接口內部data值爲nil,但tab不爲空時,此時接口爲nil interface。
// 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 interface實現分析--小米雲技術

深度解密Go語言之關於 interface 的10個問題

Go Interface 源碼剖析

相關文章
相關標籤/搜索