Go语言模型:通过runtime源码和汇编看interface的底层实现

Go的汇编

要看懂Go的内存模型,需要对Go runtime的源码和Go的汇编指令有一定的了解。Go的汇编是基于 Pan9 汇编的风格。Go的实现有大量的汇编代码,比如:goroutine的上下文切换肯定是要汇编的,切换栈帧和寄存器,这些是无法通过简单的function call来完成的,操作系统的线程上下文切换同样类似。

在Linux平台,可以通过看Go runtime的源码结合GDB(v7.1+)调试来快速理解一些Go关键的内存模型和运行原理,从而更加深刻的理解Go的语言特性。

Go的接口

在Go 1.10版中,Go interface 的实现在 runtime2.go 中,定义如下:

// 可以利用Goland IDE双击shift快速搜索到
type iface struct {
	tab  *itab           // 有类型信息、函数指针表等
	data unsafe.Pointer  // 类似C的void*
}

// 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
	unused int32
	fun    [1]uintptr // variable sized
}

通过上面的源码,我们可以看到Go的接口类型本质也是一个结构体,里面是两个指针,一个指向类型+函数等信息,一个指向数据信息。那么显而易见,简单来说,一个结构体实现了一个接口,把这个结构体变量赋值给这个接口变量,那么显然这个接口变量里的俩指针,就完成了数据和实现的绑定。

一个小例子:

// @file: hi.go
package main

import (
	"fmt"
)

type Printer interface {
	Print()
}

type User struct {
	id   int
	name string
}

func (u *User) Print() {
	fmt.Println(u.id, u.name)
}

func main() {
	a := &User{
		id:   1,
		name: "Michel",
	}
	var m Printer = a
	m.Print()
}

编译时带上-gcflags "-N -l"忽略优化,然后用gdb分析如下:

go build -gcflags "-N -l" hi.go
gdb hi   // gdb 拉起进程, 断main.main然后next到接口变量m赋值,或者直接断对应行
(gdb) set pr pr on // 优化输出

// 接口的数据域
(gdb) p m
$4 = {
  tab = 0x4c3a20 <User,main.Printer>,
  data = 0xc42007c020 // 结构数据指针 == 对应结构体指针a
}
(gdb) p a
$5 = (struct main.User *) 0xc42007c020

// 接口的函数表
(gdb) p m.tab
$6 = (runtime.itab *) 0x4c3a20 <User,main.Printer>
(gdb) p *m.tab
$7 = {
  inter = 0x49b740,
  _type = 0x4991c0,
  hash = 1834220034,
  _ = "\000\000\000",
  fun = {4727024} // hex(4727024) = 0x00000000004820f0, 函数的入口地址
}
(gdb) s   // step in 这个函数,看入口地址
main.(*User).Print (u=0xc42007c020) at /media/sf_VMshare/go_workspace/src/hi.go:16
16	func (u *User) Print() {
(gdb) disass
Dump of assembler code for function main.(*User).Print:
=> 0x00000000004820f0 <+0>:	mov    %fs:0xfffffffffffffff8,%rcx

// 接口赋值的地方的反汇编
25		var m Printer = a
=> 0x00000000004822d8 <+120>:	mov    %rax,0x20(%rsp)
   0x00000000004822dd <+125>:	lea    0x4173c(%rip),%rcx        # 0x4c3a20 <go.itab.*main.User,main.Printer>
   0x00000000004822e4 <+132>:	mov    %rcx,0x28(%rsp)
   0x00000000004822e9 <+137>:	mov    %rax,0x30(%rsp)

这样,通过看Go runtime的源码,加上gdb调试,就很容易理解接口的底层实现原理了,进一步的Go进程内存布局也可以用同样方法深入查看。

参考

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值