站长网 语言 Go内存中的接口种类

Go内存中的接口种类

前言抽象来讲,接口,是一种约定,是一种约束,是一种协议。在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 参数类型地址:%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)处。

本文来自网络,不代表站长网立场,转载请注明出处:https://www.tzzz.com.cn/html/biancheng/yuyan/2021/1103/18892.html

作者: dawei

【声明】:站长网内容转载自互联网,其相关言论仅代表作者个人观点绝非权威,不代表本站立场。如您发现内容存在版权问题,请提交相关链接至邮箱:bqsm@foxmail.com,我们将及时予以处理。
联系我们

联系我们

0577-28828765

在线咨询: QQ交谈

邮箱: xwei067@foxmail.com

工作时间:周一至周五,9:00-17:30,节假日休息

返回顶部