空接口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 赋值的时候。
参考资料