本文共 2295 字,大约阅读时间需要 7 分钟。
要看懂Go的内存模型,需要对Go runtime的源码和Go的汇编指令有一定的了解。Go的汇编是基于 的风格。Go的实现有大量的汇编代码,比如:goroutine的上下文切换肯定是要汇编的,切换栈帧和寄存器,这些是无法通过简单的function call来完成的,操作系统的线程上下文切换同样类似。
在Linux平台,可以通过看Go runtime的源码结合GDB(v7.1+)调试来快速理解一些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.gopackage mainimport ( "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.gogdb hi // gdb 拉起进程, 断main.main然后next到接口变量m赋值,或者直接断对应行(gdb) set pr pr on // 优化输出// 接口的数据域(gdb) p m$4 = { tab = 0x4c3a20, data = 0xc42007c020 // 结构数据指针 == 对应结构体指针a}(gdb) p a$5 = (struct main.User *) 0xc42007c020// 接口的函数表(gdb) p m.tab$6 = (runtime.itab *) 0x4c3a20 (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:1616 func (u *User) Print() { (gdb) disassDump 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 0x00000000004822e4 <+132>: mov %rcx,0x28(%rsp) 0x00000000004822e9 <+137>: mov %rax,0x30(%rsp)
这样,通过看Go runtime的源码,加上gdb调试,就很容易理解接口的底层实现原理了,进一步的Go进程内存布局也可以用同样方法深入查看。
转载地址:http://mbqli.baihongyu.com/