【转】Go Interface 源码剖析

源网址:http://legendtkl.com/2017/07/01/golang-interface-implement/

0.引言

在上一篇文章 《深入理解 Go Interface》中从设计和使用的角度介绍了 Golang 的 interface,作为补充,这篇文章将从源码级别来看看 interface 的具体实现。所有源码分析都是基于 Go 1.8.3。在开始之前,我们先看一个例子。

func Foo(x interface{}) {
    if x == nil {
        fmt.Println("empty interface")
        return
    }
    fmt.Println("non-empty interface")
}

func main() {
    var x *int = nil
    Foo(x)
}

如果你对于上面的输出结果有疑惑,那么不妨带着疑问来看这篇文章。上面的例子的输出结果如下

$ go run test_interface.go
non-empty interface

1. interface 底层结构

根据 interface 是否包含有 method,底层实现上用两种 struct 来表示:iface 和 eface。eface表示不含 method 的 interface 结构,或者叫 empty interface。对于 Golang 中的大部分数据类型都可以抽象出来 _type 结构,同时针对不同的类型还会有一些其他信息。

iface 表示 non-empty interface 的底层实现。相比于 empty interface,non-empty 要包含一些 method。method 的具体实现存放在 itab.fun 变量里。如果 interface 包含多个 method,这里只有一个 fun 变量怎么存呢?这个下面再细说。

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

type _type struct {
    size       uintptr // type size
    ptrdata    uintptr // size of memory prefix holding all pointers
    hash       uint32  // hash of type; avoids computation in hash tables
    tflag      tflag   // extra type information flags
    align      uint8   // alignment of variable with this type
    fieldalign uint8   // alignment of struct field with this type
    kind       uint8   // enumeration for C
    alg        *typeAlg  // algorithm table
    gcdata    *byte    // garbage collection data
    str       nameOff  // string form
    ptrToThis typeOff  // type for pointer to this type, may be zero
}
type iface struct {
    tab  *itab
    data unsafe.Pointer
}

// layout of Itab known to compilers
// allocated in non-garbage-collected memory
// Needs to be in sync with
// ../cmd/compile/internal/gc/reflect.go:/^func.dumptypestructs.
type itab struct {
    inter  *interfacetype
    _type  *_type
    link   *itab
    bad    int32
    inhash int32      // has this itab been added to hash?
    fun    [1]uintptr // variable sized
}

我们使用实际程序来看一下。

 1 package main
 2 
 3 import (
 4     "fmt"
 5 )
 6 
 7 type MyInterface interface {
 8     Print()
 9 }
10 
11 type MyStruct struct{}
12 func (ms MyStruct) Print() {}
13 
14 func main() {
15     x := 1
16     var y interface{} = x
17     var s MyStruct
18     var t MyInterface = s
19     fmt.Println(y, z)
20 }

查看汇编代码。

$ go build -gcflags '-l' -o interface11 interface11.go
$ go tool objdump -s "main\.main" interface11
TEXT main.main(SB) /Users/kltao/code/go/examples/interface11.go
    interface11.go:15   0x10870f0   65488b0c25a0080000  GS MOVQ GS:0x8a0, CX
    interface11.go:15   0x10870f9   483b6110        CMPQ 0x10(CX), SP
    interface11.go:15   0x10870fd   0f86de000000        JBE 0x10871e1
    interface11.go:15   0x1087103   4883ec70        SUBQ $0x70, SP
    interface11.go:15   0x1087107   48896c2468      MOVQ BP, 0x68(SP)
    interface11.go:15   0x108710c   488d6c2468      LEAQ 0x68(SP), BP
    interface11.go:17   0x1087111   48c744243001000000  MOVQ $0x1, 0x30(SP)
    interface11.go:17   0x108711a   488d057fde0000      LEAQ 0xde7f(IP), AX
    interface11.go:17   0x1087121   48890424        MOVQ AX, 0(SP)
    interface11.go:17   0x1087125   488d442430      LEAQ 0x30(SP), AX
    interface11.go:17   0x108712a   4889442408      MOVQ AX, 0x8(SP)
    interface11.go:17   0x108712f   e87c45f8ff      CALL runtime.convT2E(SB)
    interface11.go:17   0x1087134   488b442410      MOVQ 0x10(SP), AX
    interface11.go:17   0x1087139   4889442438      MOVQ AX, 0x38(SP)
    interface11.go:17   0x108713e   488b4c2418      MOVQ 0x18(SP), CX
    interface11.go:17   0x1087143   48894c2440      MOVQ CX, 0x40(SP)
    interface11.go:19   0x1087148   488d15b1000800      LEAQ 0x800b1(IP), DX
    interface11.go:19   0x108714f   48891424        MOVQ DX, 0(SP)
    interface11.go:19   0x1087153   488d542430      LEAQ 0x30(SP), DX
    interface11.go:19   0x1087158   4889542408      MOVQ DX, 0x8(SP)
    interface11.go:19   0x108715d   e8fe45f8ff      CALL runtime.convT2I(SB)

代码 17 行 var y interface{} = x 调用了函数 runtime.convT2E ,将 int 类型的 x 转换成 empty interface。代码 19 行 var t MyInterface = s 将 MyStruct 类型转换成 non-empty interface: MyInterface。

func convT2E(t *_type, elem unsafe.Pointer) (e eface) {
    ...
  
    x := newobject(t)
    typedmemmove(t, x, elem)
    e._type = t
    e.data = x
    return
}

func convT2I(tab *itab, elem unsafe.Pointer) (i iface) {
    t := tab._type
    
    ...
  
    x := newobject(t)
    typedmemmove(t, x, elem)
    i.tab = tab
    i.data = x
    return
}

看上面的函数原型,可以看出中间过程编译器将根据我们的转换目标类型的 empty interface 还是 non-empty interface,来对原数据类型进行转换(转换成 <*_type, unsafe.Pointer> 或者 <*itab, unsafe.Pointer>)。这里对于 struct 满不满足 interface 的类型要求(也就是 struct 是否实现了 interface 的所有 method),是由编译器来检测的。

2. itab

iface 结构中最重要的是 itab 结构。itab 可以理解为 pair\ 。当然 itab 里面还包含一些其他信息,比如 interface 里面包含的 method 的具体实现。下面细说。itab 的结构如下。

type itab struct {
    inter  *interfacetype
    _type  *_type
    link   *itab
    bad    int32
    inhash int32      // has this itab been added to hash?
    fun    [1]uintptr // variable sized
}

其中 interfacetype 包含了一些关于 interface 本身的信息,比如 package path,包含的 method。上面提到的 iface 和 eface 是数据类型(built-in 和 type-define)转换成 interface 之后的实体的 struct 结构,而这里的 interfacetype 是我们定义 interface 时候的一种抽象表示。

type interfacetype struct {
    typ     _type
    pkgpath name
    mhdr    []imethod
}

type imethod struct {   //这里的 method 只是一种函数声明的抽象,比如  func Print() error
    name nameOff
    ityp typeOff
}

_type 表示 concrete type。fun 表示的 interface 里面的 method 的具体实现。比如 interface type 包含了 method A, B,则通过 fun 就可以找到这两个 method 的具体实现。这里有个问题 fun 是长度为 1 的 uintptr 数组,那么怎么表示多个 method 呢?看一下测试程序。

package main

type MyInterface interface {
    Print()
    Hello()
    World()
    AWK()
}

func Foo(me MyInterface) {
    me.Print()
    me.Hello()
    me.World()
    me.AWK()
}

type MyStruct struct {}

func (me MyStruct) Print() {}
func (me MyStruct) Hello() {}
func (me MyStruct) World() {}
func (me MyStruct) AWK() {}

func main() {
    var me MyStruct
    Foo(me)
}

看一下函数调用对应的汇编代码。

$ go build -gcflags '-l' -o interface8 interface8.go
$ go tool objdump -s "main\.Foo" interface8
TEXT main.Foo(SB) /Users/kltao/code/go/examples/interface8.go
    interface8.go:10    0x104c060   65488b0c25a0080000  GS MOVQ GS:0x8a0, CX
    interface8.go:10    0x104c069   483b6110        CMPQ 0x10(CX), SP
    interface8.go:10    0x104c06d   7668            JBE 0x104c0d7
    interface8.go:10    0x104c06f   4883ec10        SUBQ $0x10, SP
    interface8.go:10    0x104c073   48896c2408      MOVQ BP, 0x8(SP)
    interface8.go:10    0x104c078   488d6c2408      LEAQ 0x8(SP), BP
    interface8.go:11    0x104c07d   488b442418      MOVQ 0x18(SP), AX
    interface8.go:11    0x104c082   488b4830        MOVQ 0x30(AX), CX //取得 Print 函数地址
    interface8.go:11    0x104c086   488b542420      MOVQ 0x20(SP), DX
    interface8.go:11    0x104c08b   48891424        MOVQ DX, 0(SP)
    interface8.go:11    0x104c08f   ffd1            CALL CX     // 调用 Print()
    interface8.go:12    0x104c091   488b442418      MOVQ 0x18(SP), AX
    interface8.go:12    0x104c096   488b4828        MOVQ 0x28(AX), CX //取得 Hello 函数地址
    interface8.go:12    0x104c09a   488b542420      MOVQ 0x20(SP), DX
    interface8.go:12    0x104c09f   48891424        MOVQ DX, 0(SP)
    interface8.go:12    0x104c0a3   ffd1            CALL CX           //调用 Hello()
    interface8.go:13    0x104c0a5   488b442418      MOVQ 0x18(SP), AX
    interface8.go:13    0x104c0aa   488b4838        MOVQ 0x38(AX), CX //取得 World 函数地址
    interface8.go:13    0x104c0ae   488b542420      MOVQ 0x20(SP), DX 
    interface8.go:13    0x104c0b3   48891424        MOVQ DX, 0(SP)
    interface8.go:13    0x104c0b7   ffd1            CALL CX           //调用 World()
    interface8.go:14    0x104c0b9   488b442418      MOVQ 0x18(SP), AX
    interface8.go:14    0x104c0be   488b4020        MOVQ 0x20(AX), AX //取得 AWK 函数地址
    interface8.go:14    0x104c0c2   488b4c2420      MOVQ 0x20(SP), CX
    interface8.go:14    0x104c0c7   48890c24        MOVQ CX, 0(SP)
    interface8.go:14    0x104c0cb   ffd0            CALL AX           //调用 AWK()
    interface8.go:15    0x104c0cd   488b6c2408      MOVQ 0x8(SP), BP
    interface8.go:15    0x104c0d2   4883c410        ADDQ $0x10, SP
    interface8.go:15    0x104c0d6   c3          RET
    interface8.go:10    0x104c0d7   e8f48bffff      CALL runtime.morestack_noctxt(SB)
    interface8.go:10    0x104c0dc   eb82            JMP main.Foo(SB)

其中 0x18(SP) 对应的 itab 的值。fun 在 x86-64 机器上对应 itab 内的地址偏移为 8+8+8+4+4 = 32 = 0x20,也就是 0x20(AX) 对应的 fun 的值,此时存放的 AWK 函数地址。然后 0x28(AX) = &Hello,0x30(AX) = &Print,0x38(AX) = &World。对的,每次函数是按字典序排序存放的。

我们再来看一下函数地址究竟是怎么写入的?首先 Golang 中的 uintptr 一般用来存放指针的值,这里对应的就是函数指针的值(也就是函数的调用地址)。但是这里的 fun 是一个长度为 1 的 uintptr 数组。我们看一下 runtime 包的 additab 函数。

func additab(m *itab, locked, canfail bool) {
    ...
    *(*unsafe.Pointer)(add(unsafe.Pointer(&m.fun[0]), uintptr(k)*sys.PtrSize)) = ifn
    ...
}

上面的代码的意思是在 fun[0] 的地址后面依次写入其他 method 对应的函数指针。熟悉 C++ 的同学可以类比 C++ 的虚函数表指针来看。

剩下的还有 bad,link,inhash。其中 bad 是一个表征 itab 状态的变量。而这里的 link 是 *itab 类型,是不是表示 interface 的嵌套呢?并不是,interface 的嵌套也是把 method 平铺而已。link 要和 inhash 一起来说。在 runtime 包里面有一个 hash 表,通过 hash[hashitab(interface_type, concrete_type)] 可以取得 itab,这是出于性能方面的考虑。主要代码如下,这里就不再赘述了。

const (
    hashSize = 1009
)

var (
    ifaceLock mutex // lock for accessing hash
    hash      [hashSize]*itab
)

func itabhash(inter *interfacetype, typ *_type) uint32 {
    // compiler has provided some good hash codes for us.
    h := inter.typ.hash
    h += 17 * typ.hash
    // TODO(rsc): h += 23 * x.mhash ?
    return h % hashSize
}

func additab(...) {
    ...
    h := itabhash(inter, typ)
    m.link = hash[h]
    m.inhash = 1
    atomicstorep(unsafe.Pointer(&hash[h]), unsafe.Pointer(m))
}

3. Type Assertion

我们知道使用 interface type assertion (中文一般叫断言) 的时候需要注意,不然很容易引入 panic。

func do(v interface{}) {
    n := v.(int)    // might panic
}

func do(v interface{}) {
    n, ok := v.(int)
    if !ok {
        // 断言失败处理
    }
}

这个过程体现在下面的几个函数上。

// The assertXXX functions may fail (either panicking or returning false,
// depending on whether they are 1-result or 2-result).
func assertI2I(inter *interfacetype, i iface) (r iface) {
    tab := i.tab
    if tab == nil {
        // explicit conversions require non-nil interface value.
        panic(&TypeAssertionError{"", "", inter.typ.string(), ""})
    }
    if tab.inter == inter {
        r.tab = tab
        r.data = i.data
        return
    }
    r.tab = getitab(inter, tab._type, false)
    r.data = i.data
    return
}
func assertI2I2(inter *interfacetype, i iface) (r iface, b bool) {
    tab := i.tab
    if tab == nil {
        return
    }
    if tab.inter != inter {
        tab = getitab(inter, tab._type, true)
        if tab == nil {
            return
        }
    }
    r.tab = tab
    r.data = i.data
    b = true
    return
}

// 类似
func assertE2I(inter *interfacetype, e eface) (r iface)
func assertE2I2(inter *interfacetype, e eface) (r iface, b bool)

4. 总结

从某种意义上来说,Golang 的 interface 也是一种多态的体现。对比其他支持多态特性的语言,实现还是略有差异,很难说谁好谁坏。

5. 参考

  1. Go Data Structure: Interfaces (注:09 年的文章)
  2. A Quick Guide to Go’s Assembler
  3. runtime: need a better itab table #20505

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏GreenLeaves

C# Encoding

之前做公司项目的时候,对于C#编码这块总是一知半解,所以打算通过这篇笔记对C#编码(Encoding)进行彻底的扫盲,关于编码和字符集的基础知识,请参考字符集和...

2637
来自专栏蘑菇先生的技术笔记

探索c#之不可变数据类型

2024
来自专栏写代码的海盗

scala与java之间的那些事

  scala与java之间的关系,我认为可以用一句话来开头:scala来源于java,但又高于java。   scala的设计者Martin Odersky就...

3635
来自专栏大内老A

ASP.NET MVC三个重要的描述对象:ControllerDescriptor

ASP.NET MVC应用的请求都是针对某个Controller的某个Action方法,所以对请求的处理最终体现在对目标Action方法的执行。而Action方...

1847
来自专栏恰同学骚年

.NET基础拾遗(3)字符串、集合和流

  众所周知,在.NET中String是引用类型,具有不可变性,当一个String对象被修改、插入、连接、截断时,新的String对象就将被分配,这会直接影响到...

951
来自专栏维C果糖

Guava 指南 之「前置条件」

前置条件 Guava 提供了很多用于进行前置条件检查的工具,我们强烈建议静态导入这些方法。 每个方法都用三种形式: 没有额外的参数。抛出的任何异常都没有错误信息...

2037
来自专栏coder修行路

Go 处理yaml类型的配置文件

5574
来自专栏程序员的SOD蜜

在C++中反射调用.NET(二) 定义数据接口 绑定委托方法 使用SOD DTO 对象 将.NET对象转换到C++结构体为何不使用序列化的问题

反射调用返回复杂对象的.NET方法 定义数据接口 上一篇在C++中反射调用.NET(一)中,我们简单的介绍了如何使用C++/CLI并且初步使用了反射调用.NET...

2167
来自专栏机器学习入门

LWC 58:724. Find Pivot Index

LWC 58:724. Find Pivot Index 传送门:724. Find Pivot Index Problem: Given an array ...

1928
来自专栏分布式系统和大数据处理

基于业务对象(列表)的排序

在上一篇文章 基于业务对象的筛选 中,我们讨论了如何实现Predicate<T>(T object)委托,自定义DateFilter 类来对业务对象进行筛选。与...

962

扫码关注云+社区