空接口interface{}
interface{} 赋值
interface{}
有点类似于 C/C++
里的 void*
,interface{}
,在 Golang 中可以存储任何数据类型:int、string、struct、function、nil、map等等所有:
1 | var a interface{} = 1 //字面1为int类型 |
由于Go中所有的变量有类型信息,因此存储到 interface{}
里也会带上类型信息,这样才可以在运行时支持反射等特性(这也是不同于void*的地方)。而且interface{}
还可以通过类型assert反转换到具体类型:
1 | var a interface{} = 1 |
空接口interface{}
底层是通过eface
结构来实现的,意思是empty interface
。eface
本质上类似一个 pair<type, data>
,其中type
存储了变量的实际类型,而data
指向变量的值。具体如下:
1 | type eface struct { |
Go1.7 源码中将变量赋值给 interface{}
是通过convT2E
实现的:
1 | func convT2E(t *_type, elem unsafe.Pointer, x unsafe.Pointer) (e eface) { |
可以看到在运行时,通过 typedmemmove
进行了内存拷贝,data
不是简单的指向原数据区。而反射里修改数据时,如果不是指针类型,修改会失败,应该也是基于这个原因:修改的只是拷贝的数据。
我们可以用以下实验试一下
1 | package main |
运行结果如下
1 | u: main.User{id:2, name:"Jack"} |
证明了代码里的拷贝实现。
interface{} 与 nil
当将 nil 赋值给 interface{} 变量时,type
和 data
域都将被赋值为 nil
, 因此其本质上是一个nil
而如果是一个其他类型的 nil 值,被赋值给 interface{}
,则其 type
是有具体类型的,只不过data
是nil,因而组合而成的 eface
结构就不是一个nil
1 | package main |
运行结果
1 | true |
不仅是空接口interface{}
是这样,其他有方法的interface
如果被赋值为一个具体类型的nil
值,本质上是不等于nil
,而只有被直接赋值为nil
,才是真正上的nil
。可以认为直接赋值字面上的nil
是类型type
和data
都为nil
的nil
。
非空 interface
非空interface赋值
非空 interface 一般用来实现类似C++的运行时的多态特性。将一个struct
变量赋值给非空interface时编译器会先做一次校验:看该struct
类型是否实现了接口所需的所有方法,如果没有,则会报错。例如
1 | type I interface { |
编译器会给出提示
1 | cannot use a (type int) as type I in assignment: |
运行时赋值底层借助接口 iface
来实现:
1 | type iface struct { |
itab
结构包含了两个类型:1)该 interface
自己的类型*interfacetype
; 2) 其data
所指向的具体接口实现的实际类型*_type
。interfacetype
是对_type
的封装,加上了一些interface
才有的数据,专门来表示interface
的具体类型。我们可以看到其mhdr
成员表示该interface
的方法集,但是注意这里只是函数原型metadata
,不是具体的函数定义,具体的函数定义是由实现接口的struct
来定义的。
相比于 empty interface
,non-empty interface
要包含实现该 interface
的method
具体定义,定义会被存放在 itab.fun
变量里。虽然 fun
数组只有一个元素,但实际赋值的时候会在内存上依次连续的存储各函数指针。
一个法国的bloger teh-cmc 的 go-internals 里通过汇编代码,详细说明了如何在运行时一个个填充itab
结构的各个成员的,有兴趣的同学可以自行查看。
当itab
结构被填充好了之后,运行时就可以通过调用convT2I
来将变量赋值给非空 interface
1 | func convT2I(tab *itab, elem unsafe.Pointer) (i iface) { |
其中x := newobject(t)
会在堆上分配一个 t 类型的对象。由此可见,不管赋值给非空interface
的变量存放在哪里,赋值操作都会在堆上重新生成一个对象,然后将对象的类型和指针存储在非空interface
里,必要时可能会引发变量逃逸。因此该转换是比较消耗性能的,看下一个benchmark
1 | type Addifier interface{ Add(a, b int32) int32 } |
直接调用和通过interface
来调用的差别很大,测试结果如下
1 | BenchmarkDirect-4 2000000000 1.77 ns/op |
非空interface动态dispatch
动态dispatch实际上就类似于C++里的多态实现,C++通过虚函数表存储了各个具体实现类的函数指针,这是编译时完成的。而运行时通过构造函数来生成指向虚函数表的虚表指针,调用的时候通过指针来查找具体应该调用虚函数表里的哪个函数。
而Go的实现方式也有些许类似,上文提到的itab.fun
结构就类似于虚表
概念,所不同的是,虚表是在运行时通过go
的runtime
来赋值的。一旦虚表被填充好,函数调用就简单的在虚表中查找了,主要的开销应该还是在interface
赋值的时候。
参考资料