空接口

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。