深刻理解Go的interface{}内部执行原理

概念补充

Go的interface是由两种类型来实现的:ifaceeface数组

iface指的是接口中申明有方法(至少1个),eface表示接口中没有申明方法缓存

后面会讲到这两个究竟是什么,因此这里须要先不用关心。函数

深刻理解

下面是一个简单的Demo,Binary实现了fmt.Stringer接口,咱们调用ToString()方法,会调用接口的String()方法。学习

// 类型
type Binary uint64

// 实现String方法,实现fmt.Stringer接口
func (i Binary) String() string {
	return strconv.FormatUint(uint64(i), 10)
}

func main() {
	b := Binary(200) // 01
  // var b Binary = Binary(200)
	ToString(b)// 02
}
func ToString(value interface{}) string {
  // 断言,转化成fmt.Stringer接口类型
	newValue, ok := value.(fmt.Stringer) //3
	if ok {
		return newValue.String()// 4
	}
	panic("The value is not implement fmt.Stringer func")
}
复制代码

大体的执行流程图下所示:ui

//01 执行的是,在内存中开辟一块内存,存放200这个值spa

image-20200619132543003

// 02 调用ToString方法,首先方法传递过程当中须要隐式将b转换成interface{}类型,实际上作的就是如下:3d

首先你们可能会关心,我就没见过这个结构,你是否是骗人的,其实有这个结构体,是在runtime/runtime2.go指针

type eface struct {
	_type *_type // 类型
	data  unsafe.Pointer //值
}
复制代码

那么如何转换的呢?code

type 指得是 Binary的类型,包含了Binary类型的全部信息(后面会介绍到)orm

data 指向的真实数据,因为咱们传递的不是指针,因此这种状况下实际上是作了一次内存拷贝(因此也就是尽量的别使用interface{}),data其实存的是拷贝的数据,若是换作是指针,其实也是拷贝了一份指针地址(这也就是reflect.Elem方法的做用)

​ 如下这几段代码所有来自于 runtime/iface.go

// 关于 unsafe.Pointer,unsafe包学习的时候介绍过
func convT2E(t *_type, elem unsafe.Pointer) (e eface) {
	if raceenabled {
		raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2E))
	}
	if msanenabled {
		msanread(elem, t.size)
	}
  // 首先会分配一块内存,内存大小为类型t的大小,下面这段话是mallocgc的介绍
  // Allocate an object of size bytes.
  // Small objects are allocated from the per-P cache's free lists.
  // Large objects (> 32 kB) are allocated straight from the heap.
	x := mallocgc(t.size, t, true)
	// TODO: We allocate a zeroed object only to overwrite it with actual data.
	// Figure out how to avoid zeroing. Also below in convT2Eslice, convT2I, convT2Islice.
  // 将elem拷贝到x 
	typedmemmove(t, x, elem)
  // eface 的类型为t,值为x
	e._type = t
	e.data = x
	return
}
复制代码

//3 其次就是到了断言的部分,那么断言到底执行了什么呢?

// inter 指的是fmt.Stringer接口类型信息
// e 就是咱们上面的的interface{} 的真实类型eface
func assertE2I2(inter *interfacetype, e eface) (r iface, b bool) {
	t := e._type
	if t == nil {
		return
	}
  // 获取tab,其实你们有可能不太理解
	tab := getitab(inter, t, true)
	if tab == nil {
		return
	}
	r.tab = tab
	r.data = e.data
	b = true
	return
}
复制代码

那么这里就须要理解什么是 iface

type iface struct {
	tab  *itab//table
	data unsafe.Pointer //值
}
复制代码

tab 又是什么?

​ tab的意思是table的意思,关于table的概念,你们能够去找找资料

​ 具备方法的语言一般属于如下两种阵营之一:为全部方法调用静态地准备表(如在C ++和Java中),或在每次调用时进行方法查找(如在Smalltalk及其许多模仿程序中,包括JavaScript和Python)以及添加奇特的缓存以提升调用效率。Go位于二者的中间:它具备方法表,但在运行时对其进行计算。

type itab struct {
	inter *interfacetype// 接口类型,这里就是Stringer
	_type *_type// 值类型, 这里就是Binary
	hash  uint32 // copy of _type.hash. Used for type switches.
	_     [4]byte
	fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}
复制代码

其实上面这段代码的流程以下:

data就是 eface.data

tab其实就是 : inter 指的是接口类型(也就是fmt.Stringer接口),type是Binary类型,fun[0]是(Binary)String方法 ,其余几个先不用care

image-20200619132703793

//4 newValue.String()到底作了啥,其实根据上面咱们很容易知道,没法就是newValue.tab.fun[0].(newValue.data) ,因此就是这么简单。

总结

一、go的 interface{} 转换过程当中至少作一次内存拷贝,因此传递指针是最好的选择。

type User struct {
	Name string
	Age  int
}

func main() {
  var empty interface{} = User{} //这里会拷贝一次,将user转换成interface{},因此函数传递过程当中也别直接使用结构体传递
}
复制代码

正确写法

func main() {
	var empty interface{} = &User{}
}
复制代码

二、有人会问到字符串传递是否内存拷贝,回答否,由于字符串底层是一个byte[] 数组,他的结构组成是

type StringHeader struct {
	Data uintptr //数据,是一个二进制数组
	Len  int// 长度
}
复制代码

因此64位计算机,字符串的长度是128位,占用16个字节

三、减小使用interface{} ,由于会有没必要要的开销,其次Golang自己是一个强类型语言,静态语言,申明式是最好的方式。

四、interface{}是反射的核心,后期我会讲解反射

参考

research.swtch.com/interfaces

相关文章
相关标签/搜索