Go内存中的接口种类
发布时间:2021-11-03 22:35:09  所属栏目:语言  来源:互联网 
            导读:前言 抽象来讲,接口,是一种约定,是一种约束,是一种协议。 在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)处。 (编辑:鹰潭站长网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! | 

