在之前的一篇文章中go语言的反射和泛型中提到过,我们可以使用反射来实现多态。反射能实现本质上是编译器同时记录的实际的数据和类型信息。在go语言中,接口是方法的集合,我们通过实现一个集合中的方法就可以隐式的实现了这个接口。也就是说我们可以把具体的类型赋值给相应的接口类型。这个时候,该变量的静态类型和动态类型是不一致的。
空接口
package main
import "fmt"
type People interface {
Name() string
}
type Student struct {
name string
}
func (stu *Student) Name() string {
return stu.name
}
func main() {
// stu的静态类型就是People,动态类型是*Student
var stu People = &Student{}
}
任意一个实现了该接口中全部方法的类型,都可以赋值给该接口类型。那么接口又是如何知道自己所指向对象的类型的呢?我们可以看一下接口的构成,我们先从简单的空接口看起。
type eface struct {
_type *_type // 动态类型
data unsafe.Pointer // 指向实际保存的数据
}
可以看到,空接口中存储了它所指向数据的动态类型和数据,有两个字段。这就解释了如下语句的输出:
func main() {
var stu *Student
var p any = stu
if p == nil {
fmt.Println("stu is nil")
} else {
fmt.Println("stu is not nil")
}
}
//输出:stu is not nil
当我们把*Student类型的指针赋值给空接口类型的变量时,该空接口的_type和data都不为空了,所以此时的输出就不再是空了。把上述的赋值语句改成:var p *Student = stu或者p := stu都会输出stu is nil,因为次数变量的静态类型和动态类型一致,就是一个普通的指针。
package main
import "fmt"
type Student struct {
name string
}
func main() {
var stu *Student
if stu == nil {
fmt.Println("stu is nil")
} else {
fmt.Println("stu is not nil")
}
}
//输出:"stu si nil"
非空接口
上面我们说过,一个接口类型可以接收任意一个实现了该接口集合中方法的类型,并根据该变量的动态类型来调用它的方法。当我调用方法的时候,编译器是怎么知道我调用的是哪个方法呢?我们可以查看非空接口的源码字段:
type iface struct {
tab *itab // 动态类型
data unsafe.Pointer // 动态类型数据地址
}
字段和含义和空接口什么区别,但实际存储的数据会更多:
type itab struct {
inter *interfacetype
_type *_type
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.
}
type interfacetype struct {
typ _type //接口的类型
pkgpath name //接口所在的包名
mhdr []imethod //接口的方法集合
}
上面的fun字段是实际记录了动态类型的函数指针列表。既然记录了函数指针,不可能每个动态类型的函数只有一个吧,但是它的列表长度缺失1。它实际上用了一个小技巧使用一个长度为1的数组来记录函数,主要目的还是为了节省内存。我们知道,数组实际上也是通过数组首地址加上偏移量来访问数组元素的,所以理论上我们只需要记录数组首地址就可以了,上面的实现也是利用了这个原理。我们可以参考一个小例子:
data := []int{1,2,3}
var pointerStore [1]uintptr
pointerStore[0] = uintptr(unsafe.Pointer(&data)) // data pointer to pointerStore[0]
// get data header pointer
var dataHeader = unsafe.Pointer(&pointerStore[0])
nums1 := unsafe.Pointer(uintptr(dataHeader) + uintptr(8))
fmt.Println(*(*int)(nums1))
nums2 := unsafe.Pointer(uintptr(dataHeader) + 2 * uintptr(8))
fmt.Println(*(*int)(nums2))
nums3 := unsafe.Pointer(uintptr(dataHeader) + 3 * uintptr(8))
fmt.Println(*(*int)(nums3))