
jasper的技术小窝
深入Golang之interface
Golang中的interface是其比较有特色的地方,其并不只是类似于Java中的Object一样,是所有对象的祖先,而且在Golang中interface还有很多不一样的特性。Go语言的主要设计者之一罗布·派克( Rob Pike)曾经说过,如果只能选择一个Golang语言的 性移植到其他语言中,他会选择interface。
概览
先来对Golang中,interface是一组method的集合,是duck-type programming的一种体现。不关心属性(数据),只关心行为(方法)。如果一个类型实现了一个 interface 中所有方法,我们说类型实现了该 interface,所以所有类型都实现了 empty interface,因为任何一种类型至少实现了0个方法。Golang没有显式的关键字用来实现interface,只需要实现interface包含的方法即可。
type MyInterface interface{ Print() } func TestFunc(x MyInterface) { fmt.Println("hh") } type MyStruct struct {} func (me MyStruct) Print() {} func main() { var me MyStruct TestFunc(me) }
在Golang中没有泛型,但是使用interface,可以达到泛型的效果;另外,如果要判断一个interface的类型,一般来说主要有两种方式,一个是使用反射reflect
,另一种是类型断言Type assertions
;具体这里就不举例了。
interface源码解析
在开始之前,我们先来用一个例子引入:
例一:
func Fun(x interface{}) { if x == nil { fmt.Println("empty") return } fmt.Println("non-empty") } func main() { var x *int Fun(x) }
运行结果为:
non-empty
例二:
type People interface { Show() } type Student struct{} func (stu *Student) Show() { } func live() People { var stu *Student return stu } func main() { a := live() a.Show() if a == nil { fmt.Println("AAAAAAA") } else { fmt.Println("BBBBBBB") } }
结果为:
BBBBBBB
带着对这个结果的疑问我们来翻翻Golang中对于interface部分的源码。
根据interface是否包含method,在底层实现上有两个struct来展示,分别为iface
(non-empty interface)和eface
(empty interface)。
eface
数据结构为:
type eface struct { _type *_type data unsafe.Pointer } type _type struct { size uintptr ptrdata uintptr hash uint32 tflag tflag align uint8 fieldalign uint8 kind uint8 alg *typeAlg gcdata *byte str nameOff ptrToThis typeOff }
eface其实就是interface{}底层使用的数据结构,其包括一个指向数据的指针data(Golang语言中特殊的指针类型unsafe.Pointer类似于c语言中的void*)和一个_type类型的结构体指针,_type存储的是该interface的类型信息;其中size描述类型的大小,hash数据的hash值,align是对齐,fieldAlgin是这个数据嵌入结构体时的对齐,kind是一个枚举值,每种类型对应了一个编号。alg是一个函数指针的数组,存储了hash/equal这两个函数操作。gcdata存储了垃圾回收的GC类型的数据,精确的垃圾回收中,就是依赖于这里的gcdata,具体的以后再说。
iface
数据结构为:
type iface struct { tab *itab data unsafe.Pointer } type itab struct { inter *interfacetype _type *_type link *itab hash uint32 bad bool inhash bool unused [2]byte fun [1]uintptr } type interfacetype struct { typ _type pkgpath name mhdr []imethod } type imethod struct { name nameOff ityp typeOff }
iface是带有方法的interface的底层数据结构,和eface大体相似,使用itab来存储相关信息,其中也包括type,多了的几个参数inter字段表示这个interface value所属的接口元信息,一个指向method的数组的指针,里面缓存了method信息,bad描述该类型有没有实现interface(也即是否实现了所有方法),fun用于存储函数地址表,这里放置和接口方法对应的具体数据类型的方法地址实现接口调用方法的动态分派,一般在每次给接口赋值发生转换时会更新此表。因此我们如果通过接口进行函数调用,实际的操作其实就是a.tab->fun[0](a.data)
下面我们有必要来详细地看一看这个itab。
接口相关的操作主要在于对其内部字段itab的操作,因为接口转换最重要的是类型信息。接口的类型转换在编译期会生成一个函数调用的语法树节点,调用runtime提供的相应接口转换函数完成接口的类型设置,所以接口的转换是在运行时发生的,其具体类型的方法地址表也是在运行时填写的。另外,由于在运行时转换会产生开销,所以对转换的itab做了缓存。具体在代码中:
//根据接口类型和实际数据类型生成itab(省略了部分代码) func getitab(inter *interfacetype, typ *_type, canfail bool) *itab { var m *itab for m = (*itab)(atomic.Loadp(unsafe.Pointer(&hash[h]))); m != nil; m = m.link { if m.inter == inter && m._type == typ { // 写入函数指针 additab(m, locked != 0, false) return m } } // 缓存中没找到则分配itab的内存: itab结构本身内存 + 末尾存方法地址表的可变长度 m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 0, &memstats.other_sys)) m.inter = inter m._type = typ // 写入函数指针 additab(m, true, canfail) return m }
具体的写入函数指针的过程,如下:
func additab(m *itab, locked, canfail bool) { // 根据method的name排序的,所以就是要遍历所有的method ni := len(inter.mhdr) nt := int(x.mcount) xmhdr := (*[1 << 16]method)(add(unsafe.Pointer(x), uintptr(x.moff)))[:nt:nt] j := 0 for k := 0; k < ni; k++ { i := &inter.mhdr[k] itype := inter.typ.typeOff(i.ityp) name := inter.typ.nameOff(i.name) ... // fun是一个长度为1的uintptr数组,在fun[0]的地址后面依次写入其他method对应的函数指针。 *(*unsafe.Pointer)(add(unsafe.Pointer(&m.fun[0]), uintptr(k)*sys.PtrSize)) = ifn } ... // 没有找到method,就会抛出TypeAssertionError的panic,经常类型转换报错就是来自这里啦 if !canfail { panic(&TypeAssertionError{"", typ.string(), inter.typ.string(), iname}) } m.bad = true }
这就是itab的最主要的两个方法,当判定一种类型是否满足某个接口时,Golang使用类型的方法集和接口所需要的方法集进行匹配,如果类型的方法集完全包含接口的方法集,则可认为该类型满足该接口。例如某类型有m个方法,某接口有n个方法,则很容易知道这种判定的时间复杂度为O(m*n),但是从什么的代码里我们知道Golang使用预先排序的方式进行优化,实际的时间复杂度为O(m+n)。
在做类型转化的时候,就会用到。例如将已有接口,转换为新的接口类型:
func convI2I(inter *interfacetype, i iface) (r iface) { tab := i.tab if tab == nil { return } if tab.inter == inter { r.tab = tab r.data = i.data return } // 获取itab r.tab = getitab(inter, tab._type, false) r.data = i.data return }
将其他非接口类型转化为接口类型时,使用convT2I
;和上面的convI2I
最大的不同是在于,非接口类型转换为接口类型时,发生了内存分配和数据拷贝。特别地,对于一些特定的类型,Golang已经有函数实现转化:convT2Islice,convT2Istring,convT2Inoptr等等,
还可以看到很多函数比如叫assertE2E,assertE2I,assertE2T等,这些函数就是对应的type assert的具体实现函数。E表示eface,I表示iface,T表示自定义的结构体或者基于内建类型创造出的类型。
nil的问题
现在我们回头来看看前面的关于interface是否是nil的问题。理解了接口的底层实现,这个问题其实也比较好理解了。需要说明的是nil在Golang中既指空值,也指空类型。对于非接口类型来说,对其赋值nil的语义是将其数据变为未初始化的状态,而给接口类型来说,还会将接口的类型信息字段itab置nil。
所以,上面的例子中,不管是empty interface还是non-empty interface,只有data为nil,tab或是_type都不为nil。只有二则都为nil时,接口才为nil。
总结
想理解interface机制的实现,只需要理解类型元数据以及动态绑定过程。而且在interface的设计上,还有很多值得称道的地方,包括method预排序,以及itab的缓存等等。