空接口
var i interface{}空接口
空接口
type eface struct {
_type *_type // 动态类型
data unsafe.Pointer // 原数据地址
}
_typechan
type chantype struct {
typ _type
elem *_type
dir uintptr
}
_type
咱们结合实例再来理解一下:
f, _ := os.Open("text.txt") // f => *os.File
var i1 interface{}
i1 = f
其中,eface 的 data 字段的值就是 f,_type 就是 *os.File类型的元数据。
我们来验证一下:
func TestInterface(t *testing.T) {
f, _ := os.Open("text.txt")
fmt.Printf("f pointer:%p\n", f)
fmt.Println("==========")
var i1 interface{}
i1 = f
ptr2 := unsafe.Pointer(&i1)
opt2 := (*[2]unsafe.Pointer)(ptr2)
fmt.Println("interface: ", opt2[0], opt2[1])
}
输出如下:
f pointer:0xc00000e038
==========
interface: 0x11091e0 0xc00000e038
我们看到 i1 这个 interface{} 变量的 data 值与 f 的地址是一样的。
非空接口
所谓非空接口,就是有抽象方法,这点相信 gopher 都知道。它的底层结构如下:
type iface struct {
tab *itab // 动态类型
data unsafe.Pointer // 动态类型数据地址
}
data 字段的意义跟空接口是一样的。再看看 itab 数据结构:
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.
}
itab
type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod
}
pkgpathmhdr
itab
fun
我们知道一个动态类型肯定有多个函数,为啥这里 fun 是一个只有一个元素空间的数组呢?我们用下面代码来讲解一下:
func TestConvertPointerToSlice(t *testing.T) {
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))
}
// 打印内容
1
2
3
这里我用容量为 1 pointerStore 存储了 data 的首地址,然后我们通过转换 pointerStore[0] 拿到首地址后,通过首地址 + int偏移量的方式,拿到了 data 里面的元素。
这是一种节约空间的小技巧。在结构体的最后一个字段放一个长度为1或0的数组,运行时根据实际需要的大小来分配内存。
我们再来看看,给一个非空接口赋值,其结构体中数据。
先看赋值:
var rw io.ReadWriter
f, _ := os.Open("eggo.txt")
rw = f
再看看字段的对应的数据:
https://www.zhihu.com/zvideo/1277535575839973376
Hash 表存储 itab
iface
type itabTableType struct {
size uintptr // length of entries array. Always a power of 2.
count uintptr // current number of filled entries.
entries [itabInitSize]*itab // really [size] large, itabInitSize = 512
}
从源码 getitab 方法中发现:
// src/runtime/iface.go
func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
......
......
t := (*itabTableType)(atomic.Loadp(unsafe.Pointer(&itabTable)))
if m = t.find(inter, typ); m != nil {
goto finish
}
lock(&itabLock)
if m = itabTable.find(inter, typ); m != nil {
unlock(&itabLock)
goto finish
}
m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 0, &memstats.other_sys))
m.inter = inter
m._type = typ
m.hash = 0
m.init()
itabAdd(m)
unlock(&itabLock)
......
......
}
通过接口定义类型与动态类型元数据 组成 hash 表的key,用来获取 itab,如果没找到就添加(最多512)个元素。
接口陷阱
看看下面用例,请问输出是什么?
type People interface {
Name()
}
type Student struct {}
func (*Student) Name() {
return
}
func NewStudent() People {
var s *Student
return s
}
func TestInterfaceTrap(t *testing.T) {
stu := NewStudent()
if stu == nil {
fmt.Println("stu is nil")
} else {
fmt.Println("stu is not nil")
}
}
NewStudent()
iface
而题中,data 字段确实是nil, 但 tab 字段不是nil, 而是 *Student。
总结
接口主要分为两类,空接口和非空接口(带方法),他们的数据结构是不一样的。
空接口比较简单,只有类型元数据和数据地址,而非空接口除此之外还承接了动态类型的数据信息。
除此之外,还要注意非空接口的判断陷阱:itab 和 data 都为 nil 的时候,非空接口才会等于 nil。