Go内存中的 接口种类
发布时间:2022-11-30 12:44:26 所属栏目:语言 来源:
导读: 前言 抽象来讲,接口,是一种约定,是一种约束,是一种协议。 在Go语言中,接口是一种语法类型,用来定义一种编程规范。 在Go语言中,接口主要有两类: 没有方法定义的空接口 有方法定义的非空接口 之
|
前言 抽象来讲,接口,是一种约定,是一种约束,是一种协议。 在Go语言中,接口是一种语法类型,用来定义一种编程规范。 在Go语言中,接口主要有两类: 没有方法定义的空接口 有方法定义的非空接口 之前,有两篇图文详细介绍了空接口对象及其类型: 【Go】内存中的空接口 【Go】再谈空接口 本文将深入探究包含方法的非空接口,以下简称接口。 环境 OS : Ubuntu 20.04.2 LTS; x86_64 Go : go version go1.16.2 linux/amd64 声明 操作系统、处理器架构、Go版本不同,均有可能造成相同的源码编译后运行时的寄存器值、内存地址、数据结构等存在差异。 本文仅包含 64 位系统架构下的 64 位可执行程序的研究分析。 本文仅保证学习过程中的分析数据在当前环境下的准确有效性。 代码清单 // interface_in_memory.go package main import "fmt" import "reflect" import "strconv" type foo interface { fmt.Stringer Foo() ree() } type fooImpl int //go:noinline func (i fooImpl) Foo() { println("hello foo") } //go:noinline func (i fooImpl) ree() { println("hello ree") } //go:noinline func (i fooImpl) String() string { return strconv.Itoa(int(i)) } func main() { impl := fooImpl(123) impl.Foo() impl.ree() fmt.Println(impl.String()) typeOf(impl) exec(impl) } //go:noinline func exec(foo foo) { foo.Foo() foo.ree() fmt.Println(foo.String()) typeOf(foo) fmt.Printf("exec 参数类型地址:%p\n", reflect.TypeOf(exec).In(0)) } //go:noinline func typeOf(i interface{}) { v := reflect.ValueOf(i) t := v.Type() fmt.Printf("类型:%s\n", t.String()) fmt.Printf("地址:%p\n", t) fmt.Printf("值 :%d\n", v.Int()) fmt.Println() } 以上代码,定义了一个包含3个方法的接口类型foo,还定义了一个fooImpl类型。在语法上,我们称fooImpl类型实现了foo接口。 运行结果 程序结构 数据结构介绍 接口数据类型的结构定义在reflect/type.go源文件中,如下所示: // 表示一个接口方法 type imethod struct { name nameOff // 方法名称相对程序 .rodata 节的偏移量 typ typeOff // 方法类型相对程序 .rodata 节的偏移量 } // 表示一个接口数据类型 type interfaceType struct { rtype // 基础信息 pkgPath name // 包路径信息 methods []imethod // 接口方法 } 其实这只是一个表象,完整的接口数据类型结构如下伪代码所示: // 表示一个接口类型 type interfaceType struct { rtype // 基础信息 pkgPath name // 包路径信息 methods []imethod // 接口方法的 slice,实际指向 array 字段 u uncommonType // 占位 array [len(methods)]imethod // 实际的接口方法数据 } 完整的结构分布图如下: 另外两个需要了解的结构体,之前文章已经多次介绍过,也在reflect/type.go源文件中,定义如下: type uncommonType struct { pkgPath nameOff // 包路径名称偏移量 mcount uint16 // 方法的数量 xcount uint16 // 公共导出方法的数量 moff uint32 // [mcount]method 相对本对象起始地址的偏移量 _ uint32 // unused } reflect.uncommonType结构体用于描述一个数据类型的包名和方法信息。对于接口类型,意义不是很大。 // 非接口类型的方法 type method struct { name nameOff // 方法名称偏移量 mtyp typeOff // 方法类型偏移量 ifn textOff // 通过接口调用时的地址偏移量;接口类型本文不介绍 tfn textOff // 直接类型调用时的地址偏移量 } reflect.method结构体用于描述一个非接口类型的方法,它是一个压缩格式的结构,每个字段的值都是一个相对偏移量。 type nameOff int32 // offset to a name type typeOff int32 // offset to an *rtype type textOff int32 // offset from top of text section nameOff 是相对程序 .rodata 节起始地址的偏移量。 typeOff 是相对程序 .rodata 节起始地址的偏移量。 textOff 是相对程序 .text 节起始地址的偏移量。 接口实现类型 从以上“运行结果”可以看到,fooImpl的类型信息位于0x4a9be0内存地址处。 关于fooImpl类型,【Go】再谈整数类型一文曾进行过非常详细的介绍,此处仅分析其方法相关内容。 查看fooImpl类型的内存数据如下: 绘制成图表如下: fooImpl类型有3个方法,我们以Foo方法来说明接口相关的底层原理。 Foo方法的相关数据如下: var Foo = reflect.method { name: 0x00000172, // 方法名称相对程序 `.rodata` 节起始地址的偏移量 mtyp: 0x00009960, // 方法类型相对程序 `.rodata` 节起始地址的偏移量 ifn: 0x000989a0, // 接口调用的指令相对程序 `.text` 节起始地址的偏移量 tfn: 0x00098160, // 正常调用的指令相对程序 `.text` 节起始地址的偏移量 } 方法名称 method.name用于定位方法的名称,即一个reflect.name对象。 Foo方法的reflect.name对象位于 0x49a172(0x00000172 + 0x49a000)地址处,毫无疑问,解析结果是Foo。 (gdb) p /x 0x00000172 + 0x49a000 $3 = 0x49a172 (gdb) x /3bd 0x49a172 0x49a172: 1 0 3 (gdb) x /3c 0x49a172 + 3 0x49a175: 70 'F' 111 'o' 111 'o' (gdb) 方法类型 method.mtyp用于定位方法的数据类型,即一个reflect.funcType对象。 Foo方法的reflect.funcType对象,其位于0x4a3960(0x00009960 + 0x49a000)地址处。 Foo方法的数据类型的字符串表示形式是func()。 (gdb) x /56bx 0x4a3960 0x4a3960: 0x08 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x4a3968: 0x08 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x4a3970: 0xf6 0xbc 0x82 0xf6 0x02 0x08 0x08 0x33 0x4a3978: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x4a3980: 0xa0 0x4a 0x4c 0x00 0x00 0x00 0x00 0x00 0x4a3988: 0x34 0x11 0x00 0x00 0x00 0x00 0x00 0x00 0x4a3990: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 (gdb) x /wx 0x4a3988 0x4a3988: 0x00001134 (gdb) x /s 0x00001134 + 0x49a000 + 3 0x49b137: "*func()" (gdb) 想要深入了解函数类型,请阅读【Go】内存中的函数。 接口方法 method.ifn字段的英文注释为function used in interface call,即调用接口方法时使用的函数。 在本例中,就是通过foo接口调用fooImpl类型的Foo函数时需要执行的指令集合。 具体来讲就是,代码清单中的exec函数内调用Foo方法需要执行的指令集合。 Foo函数的method.ifn = 0x000989a0,计算出其指令集合位于地址0x4999a0(0x000989a0 + 0x401000)处。 通过内存数据可以清楚地看到,接口方法的符号是main.(*fooImpl).Foo。该函数主要做了两件事: 检查panic 在0x4999d7地址处调用另一个函数main.fooImpl.Foo。 类型方法 method.tfn字段的英文注释为function used for normal method call,即正常方法调用时使用的函数。 在本例中,就是通过fooImpl类型的对象调用Foo函数时需要执行的指令集合。 具体来讲就是,代码清单中的main函数内调用Foo方法需要执行的指令集合。 Foo函数的method.tfn = 0x00098160,计算出其指令集合位于地址0x499160(0x00098160 + 0x401000)处。 (编辑:我爱制作网_沈阳站长网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |
站长推荐



浙公网安备 33038102330576号