解密 Go interface 的类型转换原理

hi, 大家好,我是 haohongfan。

可能你看过的 interface 剖析的文章比较多了,这些文章基本都是从汇编角度分析类型转换或者动态转发。不过随着 Go 版本升级,对应的 Go 汇编也发生了巨大的变化,如果单从汇编角度去分析 interface 变的非常有难度,本篇文章我会从内存分配+汇编角度切入 interface,去了解 interface 的原理。

限于篇幅 interface 有关动态转发和反射的内容,请关注后续的文章。本篇文章主要是关于类型转换。

eface
iface

eface

func main() {
   var ti interface{}
   var a int = 100
   ti = a
   fmt.Println(ti)
}

这段最常见的代码,现在提出一些问题:

  • 如何查看 ti 是 eface 还是 iface ?

  • 值 100 保存在哪里了 ?

  • 如何看 ti 的真实的值的类型 ?

大部分源码分析都是从汇编入手来看的,这里也把对应的汇编贴出来

0x0040 00064 (main.go:44)	MOVQ	$100, (SP)
0x0048 00072 (main.go:44)	CALL	runtime.convT64(SB)
0x004d 00077 (main.go:44)	MOVQ	8(SP), AX
0x0052 00082 (main.go:44)	MOVQ	AX, ""..autotmp_3+64(SP)
0x0057 00087 (main.go:44)	LEAQ	type.int(SB), CX
0x005e 00094 (main.go:44)	MOVQ	CX, "".ti+72(SP)
0x0063 00099 (main.go:44)	MOVQ	AX, "".ti+80(SP)

这段汇编有下面这些特点:

  • CALL runtime.convT64(SB):将 100 作为 runtime.convT64 的参数,该函数申请了一段内存,将 100 放入了这段内存里

  • 将类型 type.int 放入到 SP+72 的位置

  • 将包含 100 的那块内存的指针,放入到 SP + 80 的位置

这段汇编从直观上来说,interface 转换成 eface 是看不出来的。这个如何观察呢?这个就需要借助 gdb 了。

再继续深究下,如何利用内存分布来验证是 eface 呢?需要另外再添加点代码。

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

type _type struct {
    size       uintptr
    ptrdata    uintptr // size of memory prefix holding all pointers
    hash       uint32
    tflag      tflag
    align      uint8
    fieldAlign uint8
    kind       uint8
    equal      func(unsafe.Pointer, unsafe.Pointer) bool
    gcdata     *byte
    str        nameOff
    ptrToThis  typeOff
}

func main() {
    var ti interface{}
    var a int = 100
    ti = a

    fmt.Println("type:", *(*eface)(unsafe.Pointer(&ti))._type)
    fmt.Println("data:", *(*int)((*eface)(unsafe.Pointer(&ti)).data))
    fmt.Println((*eface)(unsafe.Pointer(&ti)))
}

output:

type: {8 0 4149441018 15 8 8 2 0x10032e0 0x10e6b60 959 27232}
data: 100
&{0x10ade20 0x1155bc0}

从这个结果上能够看出来

  • eface.kind = 2, 对应着 runtime.kindInt

  • eface.data = 100

从内存上分配上看,我们基本看出来了 eface 的内存布局及对应的最终的 eface 的类型转换结果。

iface

package main

type Person interface {
   Say() string
}

type Man struct {
}

func (m *Man) Say() string {
   return "Man"
}

func main() {
    var p Person

    m := &Man{}
    p = m
    println(p.Say())
}

iface 我们也看下汇编:

0x0029 00041 (main.go:24)	LEAQ	runtime.zerobase(SB), AX
0x0030 00048 (main.go:24)	MOVQ	AX, ""..autotmp_6+48(SP)
0x0035 00053 (main.go:24)	MOVQ	AX, "".m+32(SP)
0x003a 00058 (main.go:25)	MOVQ	AX, ""..autotmp_3+64(SP)
0x003f 00063 (main.go:25)	LEAQ	go.itab.*"".Man,"".Person(SB), CX
0x0046 00070 (main.go:25)	MOVQ	CX, "".p+72(SP)
0x004b 00075 (main.go:25)	MOVQ	AX, "".p+80(SP)

这段汇编上,能够看出来是有 itab 的,但是是否真的是转成了 iface,汇编上仍然反应不出来。

同样,我们继续用 gdb 查看 Person interface 确实被转换成了 iface。

关于 iface 内存布局,我们仍然加点代码来查看

type itab struct {
    inter *interfacetype
    _type *_type
    hash  uint32
    _     [4]byte
    fun   [1]uintptr
}

type iface struct {
    tab  *itab
    data unsafe.Pointer
}

type Person interface {
    Say() string
}

type Man struct {
    Name string
}

func (m *Man) Say() string {
    return "Man"
}

func main() {
    var p Person

    m := &Man{Name: "hhf"}
    p = m
    println(p.Say())

    fmt.Println("itab:", *(*iface)(unsafe.Pointer(&p)).tab)
    fmt.Println("data:", *(*Man)((*iface)(unsafe.Pointer(&p)).data))
}

output:

Man
itab: {0x10b3ba0 0x10b1900 1224794265 [0 0 0 0] [17445152]}
data: {hhf}

关于想继续探究 eface, iface 的内存布局的同学,可以基于上面的代码,利用 unsafe 的相关函数去看对应的内存位置上的值。

类型断言

type Person interface {
   Say() string
}

type Man struct {
   Name string
}

func (m *Man) Say() string {
   return "Man"
}

func main() {
   var p Person

    m := &Man{Name: "hhf"}
    p = m

    if m1, ok := p.(*Man); ok {
      fmt.Println(m1.Name)
    }
}

我们仅关注类型断言那块内容,贴出对应的汇编

0x0087 00135 (main.go:23)	MOVQ	"".p+104(SP), AX
0x008c 00140 (main.go:23)	MOVQ	"".p+112(SP), CX
0x0091 00145 (main.go:23)	LEAQ	go.itab.*"".Man,"".Person(SB), DX
0x0098 00152 (main.go:23)	CMPQ	DX, AX

能够看出来的是:将 iface.itab 放入了 AX,将 go.itab.*"".Man,"".Person(SB) 放入了 DX,比较两者是否相等,来判断 Person 的真实类型是否是 Man。

另外一个类型断言的方式就是 switch 了,其实两者本质上没啥区别。

interface 最著名的坑的,应该就是下面这个了。

func main() {
    var a interface{} = nil
    var b *int = nil
    
    isNil(a)
    isNil(b)
}

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

output:

empty interface
non-empty interface

为什么会这样呢?这就涉及到 interface == nil 的判断方式了。一般情况只有 eface 的 type 和 data 都为 nil 时,interface == nil 才是 true。

当我们把 b 复制给 interface 时,x._type.Kind = kindPtr。虽说 x.data = nil,但是不符合 interface == nil 的判断条件了。

关于 interface 源码阅读的一点建议

关于 interface 源码阅读的一点建议,如果想利用汇编看源码的话,尽量选择 go1.14.x。

选择 Go 汇编来看 interface,基本上也是为了查看 interface 最终被转换成 eface 还是 iface,调用了 runtime 的哪些函数,以及对应的函数栈分布。如果 Go 版本选择的太高的话,go 汇编变化太大了,可能汇编上就看不到对应的内容了。

以上分享来自,京东资深架构师 haohongfan ,大佬最近也开了公众号记录和分享他的经验总结,点击下方名片关注。

也欢迎关注我的公众号,我会一直为大家持续推荐优秀的技术文章和面经。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值