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
}

//========================
//这两个接口共同的字段_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类型是如何转换成接口类型的呢?
看一下生成的汇编代码:

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
}

从以上实现我们发现编译器生成的struct原始数据会复制一份,然后将新的数据地址赋值给iface.data从而生成了完整的iface,这样如下原始代码中的xh就转换成了Person接口类型。

   var xh Person = xitehip{age:18}

用gdb实际运行看一下(见图1):


图1

convT2Inoptr函数传进来的参数是*itab和源码中的 *xitehip。
图2是itab的类型原型和内存中的数据发现itab确实是runtime中源码里的字段。总共占了32个字节。([4]uint8 不占字节)


图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 指针变量类型是如何转换成接口类型的呢?

还是上面的例子只是将

   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这个结构体没有被复制。
用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 源码剖析

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容