GoLang 接口源码解析

2019-03-28 language golang

空接口

空接口类型实现没有要求,可以将任意一个值赋给空接口类型的变量,然后通过类型断言判断实际的接口类型。

var any interface{}
any = true
any = 3.1415926
any = "Hello World"
any = map[string]int{"one":1}
any = new(bytes.Buffer)

空接口实际是在非空接口的实现上进行了优化,对应了 eface (也就是空接口) 的实现。

type eface struct {
	_type *_type          // 类型信息
	data  unsafe.Pointer
}

其中的 _type 类型定义如下。

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
}

标识类型信息,包括了什么类型、类型大小、对齐信息等。

非空接口实现

// runtime/runtime2.go
type iface struct {
	tab  *itab
	data unsafe.Pointer
}

其中 tab 成员指向的结构体,包括了类型的元数据以及一些函数的列表。

// runtime/runtime2.go
type itab struct {
	inter *interfacetype // 接口信息,包含了接口信息
	_type *_type         // 类型信息
	hash  uint32         // copy of _type.hash. Used for type switches.
	_     [4]byte
	fun   [1]uintptr     // 该类型的方法(函数)列表,含接口
}

其中 interfacetype 包含了接口的定义信息,例如接口类型、包名称、包含方法名。

// runtime/type.go
type interfacetype struct {
   typ     _type       // 接口类型
   pkgpath name        // 包路径
   mhdr    []imethod   // 接口方法(函数)列表,按字典序排序
}

type imethod struct {
   name nameOff        // 方法名
   ityp typeOff        // 描述方法参数返回值等细节  ???? 函数的签名如何实现
}

也就是说,非接口除了需要保存对象之外,还会保存接口类型 (通过 interfacetype 定义) 。

当调用接口方法时,实际上调用的就是 s.itab->fun[0] 函数,这里只保存了接口中定义的方法 (即使结构体可能定义了多个方法),如果接口中没有定义,那么会直接报错。

package main

import "fmt"

type Shape interface {
        Area() float64
}

type Rectangle struct {
        Width, Height float64
}

func (r *Rectangle) Area() float64 {
        return r.Height * r.Width
}

func (r *Rectangle) String() string {
        return fmt.Sprintf("width %g height %g", r.Width, r.Height)
}

func main() {
        var s Shape

        s = &Rectangle{Width: 10, Height: 20}
        fmt.Println(s.Area())
        //fmt.Println(s.String())
}

在上述注释掉的代码中,因为 Shape 接口没有定义 String() 方法,所以在编译阶段就会报错 s.String undefined (type Shape has no field or method String)

源码研究

赋值划分为两种:A) struct 类型,可能含有函数实现;B) 常规类型,例如 int string 等。

eface

也就是会调用 convT2E64() 函数创建一个 eface 对象。

package main

import "fmt"

func main() {
        var x int
        var y interface{}

        x = 1
        y = x
        fmt.Println(y)
}

其中关键的汇编代码如下,也就是通过 convT2E64() 函数

0x001d 00029 (simple.go:10)     LEAQ    type.int(SB), AX   # 取int类型的地址
0x0024 00036 (simple.go:10)     PCDATA  $2, $0
0x0024 00036 (simple.go:10)     MOVQ    AX, (SP)           # 准备调用函数前的栈帧
0x0028 00040 (simple.go:10)     MOVQ    $1, 8(SP)
0x0031 00049 (simple.go:10)     CALL    runtime.convT2E64(SB) # 创建空接口

其中 convT2E64() 函数对应的代码如下。

func convT2E64(t *_type, val uint64) (e eface) {
        var x unsafe.Pointer
        if val == 0 {
                x = unsafe.Pointer(&zeroVal[0])
        } else {
                x = mallocgc(8, t, false)
                *(*uint64)(x) = val
        }
        e._type = t
        e.data = x
        return
}

该函数中,对 0 值做了优化,可以缓存 maxZero 大小的零值,其中 mallocgc() 用来分配内存。

有些文章指出,如果值可以被一个指针的地址所保存,那么会直接使用,无需指针,不过测试使用的还是指针。

package main

import (
        "fmt"
        "unsafe"
)

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

func main() {
        v := 10
        var a interface{} = v

        var f eface = *(*eface)(unsafe.Pointer(&a))
        fmt.Println(f.data, ":", *(*int)(f.data))
}

输出的是 0xc000014098 : 10

iface

也就是会调用 convT2Inoptr() 函数创建一个 eface 对象。

package main

import "fmt"

type Shape interface {
        Area() float64
}

type Rectangle struct {
        Width, Height float64
}

func (r Rectangle) Area() float64 {
        return r.Height * r.Width
}

func main() {
        var s Shape

        r := Rectangle{Width: 10, Height: 20}
        s = r
        fmt.Println(s)
}

同样汇编后的关键代码如下。

0x003d 00061 (simple.go:21)     LEAQ    go.itab."".Rectangle,"".Shape(SB), AX
0x0044 00068 (simple.go:21)     PCDATA  $2, $0
0x0044 00068 (simple.go:21)     MOVQ    AX, (SP)
0x0048 00072 (simple.go:21)     PCDATA  $2, $1
0x0048 00072 (simple.go:21)     LEAQ    ""..autotmp_2+48(SP), AX  // 将r变量的地址保存到AX
0x004d 00077 (simple.go:21)     PCDATA  $2, $0
0x004d 00077 (simple.go:21)     MOVQ    AX, 8(SP)
0x0052 00082 (simple.go:21)     CALL    runtime.convT2Inoptr(SB)

其中 convT2Inoptr() 函数的实现如下。

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)
        memmove(x, elem, t.size)
        i.tab = tab
        i.data = x
        return
}

也就是说,返回变量的接口类型是没有变,始终为 Shape 接口类型,只是其对应的值修改了。

接口转换原理

在上述的 iface 源码解析中,实际封装了接口的类型 interfacetype 和实体类型的类型 _type,当判断某个类型是否满足接口时,会将类型与接口的方法集进行匹配,如果包含则认为实现了该接口。

假设类型有 m 个方法,接口有 n 个方法,那么如果遍历则其时间复杂度为 O(mn),在实现时,实际会对方法集的函数按照函数名的字典序排序,所以优化后的实际时间复杂度为 O(m+n)

源码解析

package main

import (
        "fmt"
)

type Stringer interface {
        String() string
}

type Shape interface {
        Area() float64
        String() string
}

type Rectangle struct {
        Width, Height float64
}

func (r *Rectangle) Area() float64 {
        return r.Height * r.Width
}

func (r *Rectangle) String() string {
        return fmt.Sprintf("width %g height %g", r.Width, r.Height)
}

func main() {
        var r Shape
        var s Stringer

        r = &Rectangle{Width: 10, Height: 20}
        fmt.Println(r.Area())

        s = r
        fmt.Println(s.String())
}

首先,将 Rectangle 结构体隐式转换为 type Shape interface 接口变量,然后又通过 s = r 进行了一次接口之间的变量转换,这里调用的是 runtime.convI2I() 函数。

func convI2I(inter *interfacetype, i iface) (r iface) {
        tab := i.tab
        if tab == nil {
                return
        }
        if tab.inter == inter { // 接口相同,直接赋值给新的返回变量即可
                r.tab = tab
                r.data = i.data
                return
        }
        r.tab = getitab(inter, tab._type, false)
        r.data = i.data
        return
}

其中 iface 是由 tabdata 两个字段组成,而 data 在入参 i 中,那么只需要找到新的 tab 就可以了。当接口类型相同时,直接简单的做个赋值即可,下面介绍不同的时候。

为了减少上述 O(m+n) 的匹配过程,会缓存到一个 Hash 表中,其中的 Key 就是通过接口类型 interfacetype 和实体类型 _type 的值组成,可以通过 getitab() 函数查找。

当没有查找到时,会通过 additab() 函数添加到 Hash 表中,这里比较关键的是如何拼装出来 itab 变量,会检查合法性,并将结构体中定义的函数指针添加到变量中,注意,是只包含了接口中定义的函数。

断言

接口的断言就是通过 type-switch 或者 v.(T) 来判断一个接口变量是否满足某个接口的要求。

package main

import "fmt"

type Shape interface {
        Area() float64
}

type Rectangle struct {
        Width, Height float64
}

func (r Rectangle) Area() float64 {
        return r.Height * r.Width
}

func main() {
        var r interface{}

        r = Rectangle{Width: 10, Height: 20}
        if v, ok := r.(Shape); ok {
                fmt.Println(v.Area())
        }

        switch r.(type) {
        case Shape:
                fmt.Println("it's a shape interface.")
        }
}

上述代码其实隐含了一个默认的类型转换,从 struct 转换到接口变量。

0x0066 00102 (simple.go:20)     MOVQ    24(SP), AX
0x006b 00107 (simple.go:20)     MOVQ    16(SP), CX
0x0070 00112 (simple.go:21)     PCDATA  $2, $2
0x0070 00112 (simple.go:21)     LEAQ    type."".Shape(SB), DX
0x0077 00119 (simple.go:21)     PCDATA  $2, $1
0x0077 00119 (simple.go:21)     MOVQ    DX, (SP)
0x007b 00123 (simple.go:21)     MOVQ    CX, 8(SP)
0x0080 00128 (simple.go:21)     PCDATA  $2, $0
0x0080 00128 (simple.go:21)     MOVQ    AX, 16(SP)
0x0085 00133 (simple.go:21)     CALL    runtime.assertE2I2(SB)