golang中interface底層分析

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
}

//========================
//這兩個(gè)接口共同的字段_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這個(gè)結(jié)構(gòu)體是golang定義數(shù)據(jù)類型要用的亏较,講到反射文章的時(shí)候在具體講解這個(gè)_type莺褒。

1.iface

1.1 變量類型是如何轉(zhuǎn)換成接口類型的?

看下方代碼:

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類型是如何轉(zhuǎn)換成接口類型的呢遵岩?
看一下生成的匯編代碼:

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

從匯編發(fā)現(xiàn)有個(gè)轉(zhuǎn)換函數(shù):
runtime.convT2Inoptr(SB)
我們?nèi)タ匆幌逻@個(gè)函數(shù)的實(shí)現(xiàn):

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申請內(nèi)存
        memmove(x, elem, t.size)//將elem所指向的數(shù)據(jù)賦值到新的內(nèi)存中
        i.tab = tab //設(shè)置iface的tab
        i.data = x //設(shè)置iface的data
        return
}

從以上實(shí)現(xiàn)我們發(fā)現(xiàn)編譯器生成的struct原始數(shù)據(jù)會(huì)復(fù)制一份,然后將新的數(shù)據(jù)地址賦值給iface.data從而生成了完整的iface巡通,這樣如下原始代碼中的xh就轉(zhuǎn)換成了Person接口類型尘执。

   var xh Person = xitehip{age:18}

用gdb實(shí)際運(yùn)行看一下(見圖1):


圖1

convT2Inoptr函數(shù)傳進(jìn)來的參數(shù)是*itab和源碼中的 *xitehip。
圖2是itab的類型原型和內(nèi)存中的數(shù)據(jù)發(fā)現(xiàn)itab確實(shí)是runtime中源碼里的字段宴凉√芏В總共占了32個(gè)字節(jié)。([4]uint8 不占字節(jié))


圖2

圖3是elem的數(shù)據(jù)他是個(gè)名為xitehip的結(jié)構(gòu)體類型里面存放的是age=18弥锄。
內(nèi)存中的0x12正好是age=18丧靡。注意此時(shí)的地址是:0xc000032777蟆沫。


圖3

圖4是xh變量的數(shù)據(jù)類型和其中data字段的數(shù)據(jù)。發(fā)現(xiàn)xh確實(shí)是iface類型了且xh.data的地址不是上面提到的0xc000032777 而是0xc000014098温治,證明是復(fù)制了一份xitehip類型的struct饭庞。
圖4

1.2 指針變量類型是如何轉(zhuǎn)換成接口類型的呢?

還是上面的例子只是將

   var xh Person = xitehip{age:18}

換成了

   var xh Person = &xitehip{age:18}

那指針類型的變量是如何轉(zhuǎn)換成接口類型的呢熬荆?
見下方匯編代碼:

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)

發(fā)現(xiàn)了這個(gè)函數(shù):

runtime.newobject(SB)

去看一下具體實(shí)現(xiàn):

// 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)
}

編譯器自動(dòng)生成了iface并將&xitehip{age:18}創(chuàng)建的對象的地址(通過newobject)賦值給iface.data舟山。就是xitehip這個(gè)結(jié)構(gòu)體沒有被復(fù)制。
用gdb看一下見圖5:


圖5

1.3 那xh是如何找到run方法的呢卤恳?我們繼續(xù)看見圖6累盗,相關(guān)解釋在圖中已經(jīng)標(biāo)注:

圖6

1.4 接口調(diào)用規(guī)則

把上面的例子添加一個(gè)eat()接口方法并實(shí)現(xiàn)它(注意這個(gè)接口方法的實(shí)現(xiàn)的接受者是指針)。

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()
}

這個(gè)例子的xh變量的實(shí)際類型是個(gè)指針突琳,那它是如何調(diào)用非指針方法run的呢若债?
繼續(xù)gdb跟蹤一下,見圖7:


圖7

直接跟蹤xh.tab.fun的內(nèi)存數(shù)據(jù)發(fā)現(xiàn)eat方法確實(shí)在0x44f940。上面已經(jīng)說了fun這個(gè)數(shù)組大小只為1那run方法應(yīng)該在eat的后面本今,但是gdb沒有提示哪個(gè)地方是run的起始位置拆座。為了驗(yàn)證run就在eat的后面,我直接往下debug看eat的入口地址在哪里,見圖8冠息。


圖8

run指令的地址是0x44fa60挪凑。那我去打印一下這個(gè)地址所指向的具體的值是什么,見圖9:
圖9

我們在看一下圖7中逛艰,為了更清楚我基于圖7再截一次圖躏碳,見圖10:
圖10

發(fā)現(xiàn)圖9和和圖10的的run方法的指令是一樣的,證明兩個(gè)方法的指令確實(shí)一起排列的散怖。

總結(jié)菇绵,指針類型的對象調(diào)用非指針類型的接收方的方法,編譯器自動(dòng)將接收方轉(zhuǎn)換為指針類型镇眷;調(diào)用方通過xh.tab.fun這個(gè)數(shù)組找到對應(yīng)的方法指令列表咬最。

那xh是值類型的接口,而接口實(shí)現(xiàn)的方法的接收方是指針類型欠动,那調(diào)用方可以調(diào)用這個(gè)指針方法嗎永乌,答案是不僅不能連編譯都編譯不過去,見圖11:


圖11

見下表總結(jié):

調(diào)用方 接收方 能否編譯
true
指針 false
指針 true
指針 指針 true
指針 指針和值 true
指針和值 false

從上表可以得出如下結(jié)論:

調(diào)用方是值時(shí)具伍,只要接收方有指針方法那編譯器不允許通過編譯翅雏。

2 eface

空接口相對于非空接口沒有了方法列表。

type eface struct {
    _type *_type
    data  unsafe.Pointer
}

第一個(gè)屬性由itab換成了_type,這個(gè)結(jié)構(gòu)體是golang中的變量類型的基礎(chǔ)人芽,所以空接口可以指定任意變量類型望几。

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斷言

判斷變量數(shù)據(jù)類型

   s, ok := i.(TypeName)
    if ok {
        fmt.Println(s)
    }

如果沒有ok的話類型不正確的話會(huì)引起panic。

也可以用switch形式:

    switch v := v.(type) {
      case TypeName:
    ...
    }

3 檢查接口

3.1 利用編譯器檢查接口實(shí)現(xiàn)

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 如果接口內(nèi)部data值為nil萤厅,但tab不為空時(shí)橄抹,此時(shí)接口為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實(shí)現(xiàn)分析--小米云技術(shù)
深度解密Go語言之關(guān)于 interface 的10個(gè)問題
Go Interface 源碼剖析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市楼誓,隨后出現(xiàn)的幾起案子矢劲,更是在濱河造成了極大的恐慌,老刑警劉巖慌随,帶你破解...
    沈念sama閱讀 212,080評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異躺同,居然都是意外死亡阁猜,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,422評論 3 385
  • 文/潘曉璐 我一進(jìn)店門蹋艺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來剃袍,“玉大人,你說我怎么就攤上這事捎谨∶裥В” “怎么了?”我有些...
    開封第一講書人閱讀 157,630評論 0 348
  • 文/不壞的土叔 我叫張陵涛救,是天一觀的道長畏邢。 經(jīng)常有香客問我,道長检吆,這世上最難降的妖魔是什么舒萎? 我笑而不...
    開封第一講書人閱讀 56,554評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮蹭沛,結(jié)果婚禮上臂寝,老公的妹妹穿的比我還像新娘。我一直安慰自己摊灭,他們只是感情好咆贬,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,662評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著帚呼,像睡著了一般掏缎。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上萝挤,一...
    開封第一講書人閱讀 49,856評論 1 290
  • 那天御毅,我揣著相機(jī)與錄音,去河邊找鬼怜珍。 笑死端蛆,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的酥泛。 我是一名探鬼主播今豆,決...
    沈念sama閱讀 39,014評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼嫌拣,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了呆躲?” 一聲冷哼從身側(cè)響起异逐,我...
    開封第一講書人閱讀 37,752評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎插掂,沒想到半個(gè)月后灰瞻,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,212評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡辅甥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,541評論 2 327
  • 正文 我和宋清朗相戀三年酝润,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片璃弄。...
    茶點(diǎn)故事閱讀 38,687評論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡要销,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出夏块,到底是詐尸還是另有隱情疏咐,我是刑警寧澤,帶...
    沈念sama閱讀 34,347評論 4 331
  • 正文 年R本政府宣布脐供,位于F島的核電站浑塞,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏政己。R本人自食惡果不足惜缩举,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,973評論 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望匹颤。 院中可真熱鬧仅孩,春花似錦、人聲如沸印蓖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,777評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽赦肃。三九已至溅蛉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間他宛,已是汗流浹背船侧。 一陣腳步聲響...
    開封第一講書人閱讀 32,006評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留厅各,地道東北人镜撩。 一個(gè)月前我還...
    沈念sama閱讀 46,406評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像队塘,于是被迫代替她去往敵國和親袁梗。 傳聞我的和親對象是個(gè)殘疾皇子宜鸯,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,576評論 2 349

推薦閱讀更多精彩內(nèi)容