Go内存中的接口种类
发布时间:2021-11-04 02:54:00 所属栏目:语言 来源:互联网
导读:前言 抽象来讲,接口,是一种约定,是一种约束,是一种协议。 在Go语言中,接口是一种语法类型,用来定义一种编程规范。 在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 参数类型地址:%pn", reflect.TypeOf(exec).In(0))
}
//go:noinline
func typeOf(i interface{}) {
v := reflect.ValueOf(i)
t := v.Type()
fmt.Printf("类型:%sn", t.String())
fmt.Printf("地址:%pn", t)
fmt.Printf("值 :%dn", 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)处。 (编辑:晋中站长网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |