正文
本文从源码的角度学习下Go接口的底层实现,以及接口赋值,反射,断言的实现原理。作为对比,用到了go1.8.6和go1.9.1两个版本。
1. eface
空接口通过eface结构体实现,位于runtime/runtime2.go:
// src/runtime/runtime2.go
// 空接口
type eface struct {
_type *_type
data unsafe.Pointer
}
空接口(eface)有两个域,所指向对象的类型信息(_type)和数据指针(data)。先看看
_type
字段:
// 所有类型信息结构体的公共部分
// src/rumtime/runtime2.go
type _type struct {
size uintptr // 类型的大小
ptrdata uintptr // size of memory prefix holding all pointers
hash uint32 // 类型的Hash值
tflag tflag // 类型的Tags
align uint8 // 结构体内对齐
fieldalign uint8 // 结构体作为field时的对齐
kind uint8 // 类型编号 定义于runtime/typekind.go
alg *typeAlg // 类型元方法 存储hash和equal两个操作。map key便使用key的_type.alg.hash(k)获取hash值
gcdata *byte // GC相关信息
str nameOff // 类型名字的偏移
ptrToThis typeOff
}
_type是go所有类型的公共描述,里面包含GC,反射等需要的细节,它决定data应该如何解释和操作,这也是它和C void*不同之处。
各个类型所需要的类型描述是不一样的,比如chan,除了chan本身外,还需要描述其元素类型,而map则需要key类型信息和value类型信息等:
// src/runtime/type.go
// ptrType represents a pointer type.
type ptrType struct {
typ _type // 指针类型
elem *_type // 指针所指向的元素类型
}
type chantype struct {
typ _type // channel类型
elem *_type // channel元素类型
dir uintptr
}
type maptype struct {
typ _type
key *_type
elem *_type
bucket *_type // internal type representing a hash bucket
hmap *_type // internal type representing a hmap
keysize uint8 // size of key slot
indirectkey bool // store ptr to key instead of key itself
valuesize uint8 // size of value slot
indirectvalue bool // store ptr to value instead of value itself
bucketsize uint16 // size of bucket
reflexivekey bool // true if k==k for all keys
needkeyupdate bool // true if we need to update key on an overwrite
}
这些类型信息的第一个字段都是
_type
(类型本身的信息),接下来是一堆类型需要的其它详细信息(如子类型信息),这样在进行类型相关操作时,可通过一个字(
typ *_type
)即可表述所有类型,然后再通过
_type.kind
可解析出其具体类型,最后通过地址转换即可得到类型完整的”_type树”,参考reflect.Type.Elem()函数:
// reflect/type.go
// reflect.rtype结构体定义和runtime._type一致 type.kind定义也一致(为了分包而重复定义)
// Elem()获取rtype中的元素类型,只针对复合类型(Array, Chan, Map, Ptr, Slice)有效
func (t *rtype) Elem() Type {
switch t.Kind() {
case Array:
tt := (*arrayType)(unsafe.Pointer(t))
return toType(tt.elem)
case Chan:
tt := (*chanType)(unsafe.Pointer(t))
return toType(tt.elem)
case Map:
// 对Map来讲,Elem()得到的是其Value类型
// 可通过rtype.Key()得到Key类型
tt := (*mapType)(unsafe.Pointer(t))
return toType(tt.elem)
case Ptr:
tt := (*ptrType)(unsafe.Pointer(t))
return toType(tt.elem)
case Slice:
tt := (*sliceType)(unsafe.Pointer(t))
return toType(tt.elem)
}
panic("reflect: Elem of invalid type")
}
2. iface
iface结构体表示非空接口:
// runtime/runtime2.go
// 非空接口
type iface struct {
tab *itab
data unsafe.Pointer
}
// 非空接口的类型信息
type itab struct {
inter *interfacetype // 接口定义的类型信息
_type *_type // 接口实际指向值的类型信息
link *itab
bad int32
inhash int32
fun [1]uintptr // 接口方法实现列表,即函数地址列表,按字典序排序
}
// runtime/type.go
// 非空接口类型,接口定义,包路径等。
type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod // 接口方法声明列表,按字典序排序
}
// 接口的方法声明
type imethod struct {
name nameOff // 方法名
ityp typeOff // 描述方法参数返回值等细节
}
非空接口(iface)本身除了可以容纳满足其接口的对象之外,还需要保存其接口的方法,因此除了data字段,iface通过tab字段描述非空接口的细节,包括接口方法定义,接口方法实现地址,接口所指类型等。iface是非空接口的实现,而不是类型定义,iface的真正类型为interfacetype,其第一个字段仍然为描述其自身类型的_type字段。
为了提高查找效率,runtime中实现(interface_type, concrete_type) -> itab(包含具体方法实现地址等信息)的hash表:
// runtime/iface.go
const (
hashSize = 1009
)
var (
ifaceLock mutex // lock for accessing hash
hash [hashSize]*itab
)
// 简单的Hash算法
func itabhash(inter *interfacetype, typ *_type) uint32 {
h := inter.typ.hash
h += 17 * typ.hash
return h % hashSize
}
// 根据interface_type和concrete_type获取或生成itab信息
func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
...
// 算出hash key
h := itabhash(inter, typ)
var m *itab
...
// 遍历hash slot链表
for m = (*itab)(atomic.Loadp(unsafe.Pointer(&hash[h]))); m != nil; m = m.link {
// 如果在hash表中找到则返回
if m.inter == inter && m._type == typ {
if m.bad {
if !canfail {
additab(m, locked != 0, false)
}
m = nil
}
...
return m
}
}
}
// 如果没有找到,则尝试生成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)
if m.bad {
return nil
}
return m
}
// 检查concrete_type是否符合interface_type 并且创建对应的itab结构体 将其放到hash表中
func additab(m *itab, locked, canfail bool) {
inter := m.inter
typ := m._type
x := typ.uncommon()
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)
iname := name.name()
ipkg := name.pkgPath()
if ipkg == "" {
ipkg = inter.pkgpath.name()
}
for ; j < nt; j++ {
t := &xmhdr[j]
tname := typ.nameOff(t.name)
// 检查方法名字是否一致
if typ.typeOff(t.mtyp) == itype && tname.name() == iname {
pkgPath := tname.pkgPath()
if pkgPath == "" {
pkgPath = typ.nameOff(x.pkgpath).name()
}
// 是否导出或在同一个包
if tname.isExported() || pkgPath == ipkg {
if m != nil {
// 获取函数地址,并加入到itab.fun数组中
ifn := typ.textOff(t.ifn)
*(*unsafe.Pointer)(add(unsafe.Pointer(&m.fun[0]), uintptr(k)*sys.PtrSize)) = ifn
}
goto nextimethod
}
}
}
// didn't find method
if !canfail {
if locked {
unlock(&ifaceLock)
}
panic(&TypeAssertionError{"", typ.string(), inter.typ.string(), iname})
}
m.bad = true
break
nextimethod:
}
if !locked {
throw("invalid itab locking")
}
// 加到Hash Slot链表中
h := itabhash(inter, typ)
m.link = hash[h]
m.inhash = true
atomicstorep(unsafe.Pointer(&hash[h]), unsafe.Pointer(m))
}
可以看到,并不是每次接口赋值都要去检查一次对象是否符合接口要求,而是只在第一次生成itab信息,之后通过hash表即可找到itab信息。
3. 接口赋值
type MyInterface interface {
Print()
}
type MyStruct struct{}
func (ms MyStruct) Print() {}
func main() {
a := 1
b := "str"
c := MyStruct{}
var i1 interface{} = a
var i2 interface{} = b
var i3 MyInterface = c
var i4 interface{} = i3
var i5 = i4.(MyInterface)
fmt.Println(i1, i2, i3, i4, i5)
}
用go1.8编译并反汇编:
$GO1.8PATH/bin/go build -gcflags '-N -l' -o tmp tmp.go
$GO1.8PATH/bin/go tool objdump -s "main\.main" tmp
...
tmp.go:18 0x1087165 e84645f8ff CALL runtime.convT2E(SB) // var i1 interface{} = a
...
tmp.go:19 0x10871bc e8ef44f8ff CALL runtime.convT2E(SB) // var i2 interface{} = b
...
tmp.go:20 0x10871f0 e86b45f8ff CALL runtime.convT2I(SB) // var i3 MyInterface = c
tmp.go:20 0x10871f5 488b442410 MOVQ 0x10(SP), AX // 返回的iface.itab地址
tmp.go:20 0x10871fa 488b4c2418 MOVQ 0x18(SP), CX // 返回的iface.data地址
tmp.go:20 0x10871ff 4889842480000000 MOVQ AX, 0x80(SP) // i3.tab = iface.itab
tmp.go:20 0x1087207 48898c2488000000 MOVQ CX, 0x88(SP) // i3.data = iface.data
tmp.go:21 0x108720f 488b842488000000 MOVQ 0x88(SP), AX
tmp.go:21 0x1087217 488b8c2480000000 MOVQ 0x80(SP), CX
tmp.go:21 0x108721f 48898c24e0000000 MOVQ CX, 0xe0(SP) // 0xe0(SP) = i3.tab
tmp.go:21 0x1087227 48898424e8000000 MOVQ AX, 0xe8(SP) // 0xe8(SP) = i3.data
tmp.go:21 0x108722f 48894c2448 MOVQ CX, 0x48(SP)
...
// var i4 interface{} = i3
tmp.go:21 0x108724b 488b8424e8000000 MOVQ 0xe8(SP), AX // 加载i3的data
tmp.go:21 0x1087253 488b4c2448 MOVQ 0x48(SP), CX // 加载i3的tab(即interfacetype地址)
tmp.go:21 0x1087258 48894c2470 MOVQ CX, 0x70(SP) // i4._type = i3.interfacetype
tmp.go:21 0x108725d 4889442478 MOVQ AX, 0x78(SP) // i4.data = i3.data
...
// var i5 = i4.(MyInterface)
tmp.go:22 0x1087299 e87245f8ff CALL runtime.assertE2I(SB)
...
可以看到编译器通过convT2E和convT2I将编译器已知的类型赋给接口(其中E代表eface,I代表iface,T代表编译器已知类型,即静态类型),编译器知晓itab的布局,会在编译期检查接口是否适配,并且生成itab信息,因此编译器生成的convT2X调用是必然成功的。
对于接口间的赋值,将iface赋给eface比较简单,直接提取eface的interfacetype和data赋给iface即可。而反过来,则需要使用接口断言,接口断言通过assertE2I, assertI2I等函数来完成,这类assert函数根据使用方调用方式有两个版本:
i5 := i4.(MyInterface) // call conv.assertE2I
i5, ok := i4.(MyInterface) // call conv.AssertE2I2
下面看一下几个常用的conv和assert函数实现:
// go1.8/src/runtime/iface.go
func convT2E(t *_type, elem unsafe.Pointer) (e eface) {
if raceenabled {
raceReadObjectPC(t, elem, getcallerpc(unsafe.Pointer(&t)), funcPC(convT2E))
}
if msanenabled {
msanread(elem, t.size)
}
if isDirectIface(t) {
// This case is implemented directly by the compiler.
throw("direct convT2E")
}
x := newobject(t)
// TODO: We allocate a zeroed object only to overwrite it with
// actual data. Figure out how to avoid zeroing. Also below in convT2I.
typedmemmove(t, x, elem)
e._type = t
e.data = x
return
}
func convT2I(tab *itab, elem unsafe.Pointer) (i iface) {
t := tab._type
if raceenabled {
raceReadObjectPC(t, elem, getcallerpc(unsafe.Pointer(&tab)), funcPC(convT2I))
}
if msanenabled {
msanread(elem, t.size)
}
if isDirectIface(t) {
// This case is implemented directly by the compiler.
throw("direct convT2I")
}
x := newobject(t)
typedmemmove(t, x, elem)
i.tab = tab
i.data = x
return
}
func assertE2I(inter *interfacetype, e eface) (r iface) {
t := e._type
if t == nil {
// explicit conversions require non-nil interface value.
panic(&TypeAssertionError{"", "", inter.typ.string(), ""})
}
r.tab = getitab(inter, t, false)
r.data = e.data
return
}
在assertE2I中,我们看到了getitab函数,即
i5=i4.(MyInterface)
中,会去判断i4的concretetype(MyStruct)是否满足MyInterface的interfacetype,由于前面我们执行过
var i3 MyInterface = c
,因此hash[itabhash(MyInterface, MyStruct)]已经存在itab,所以无需再次检查接口是否满足,从hash表中取出itab即可(里面针对接口的各个方法实现地址都已经初始化完成)。
而在go1.9中,有一些优化:
1.对convT2x针对简单类型(如int32,string,slice)进行特例化优化(避免typedmemmove):
convT2E16, convT2I16
convT2E32, convT2I32
convT2E64, convT2I64
convT2Estring, convT2Istring
convT2Eslice, convT2Islice
convT2Enoptr, convT2Inoptr
据统计,在编译make.bash的时候,有93%的convT2x调用都可通过以上特例化优化。参考
这里
。
2.优化了剩余对convT2I的调用
由于itab由编译器生成(参考上面go1.8生成的汇编代码和convT2I函数),可以直接由编译器将itab和elem直接赋给iface的tab和data字段,避免函数调用和typedmemmove。关于此优化可参考
1
和
2
。
具体汇编代码不再列出,感兴趣的同学可以自己尝试。
4. 类型反射
类型反射无非就是将eface{}的_type和data字段取出进行解析,针对TypeOf的实现很简单:
// 代码位于relect/type.go
// reflect.Type接口的实现为: reflect.rtype
// reflect.rtype结构体定义和runtime._type一样,只是实现了reflect.Type接口,实现了一些诸如Elem(),Name()之类的方法:
func TypeOf(i interface{}) Type {
// emptyInterface结构体定义与eface一样,都是两个word(type和data)
eface := *(*emptyInterface)(unsafe.Pointer(&i))
return toType(eface.typ)
}
// reflect.Type.Elem()仅对复合类型有效(Array,Ptr,Map,Chan,Slice),取出其中的子类型
func (t *rtype) Elem() Type {
switch t.Kind() {
case Array:
tt := (*arrayType)(unsafe.Pointer(t))
return toType(tt.elem)
case Chan:
tt := (*chanType)(unsafe.Pointer(t))
return toType(tt.elem)
case Map:
tt := (*mapType)(unsafe.Pointer(t))
// 对mapType来说,tt.elem实际上是value的类型,可通过t.Key()来获取key类型
return toType(tt.elem)
case Ptr:
tt := (*ptrType)(unsafe.Pointer(t))
return toType(tt.elem)
case Slice:
tt := (*sliceType)(unsafe.Pointer(t))
return toType(tt.elem)
}
panic("reflect: Elem of invalid type")
}
reflect.ValueOf则要复杂一些,因为它需要根据type来决定数据应该如何被解释,因此实际上reflect.Value也包含类型信息,并且通过一个flag字段来标识只读属性,是否为指针等。
type Value struct {
// 值的类型
typ *rtype
// 立即数或指向数据的指针
ptr unsafe.Pointer
// type flag uintptr
// 指明值的类型,是否只读,ptr字段是否是指针等
flag
}
func ValueOf(i interface{}) Value {
if i == nil {
return Value{}
}
escapes(i)
return unpackEface(i)
}
// 将数据从interface{}解包为reflec.Value
func unpackEface(i interface{}) Value {
e := (*emptyInterface)(unsafe.Pointer(&i))
// NOTE: don't read e.word until we know whether it is really a pointer or not.
t := e.typ
if t == nil {
return Value{}
}
f := flag(t.Kind())
if ifaceIndir(t) {
f |= flagIndir
}
return Value{t, e.word, f}
}
// 将数据由reflect.Value打包为interface{}
func packEface(v Value) interface{} {
t := v.typ
var i interface{}
e := (*emptyInterface)(unsafe.Pointer(&i))
// First, fill in the data portion of the interface.
switch {
case ifaceIndir(t):
if v.flag&flagIndir == 0 {
panic("bad indir")
}
ptr := v.ptr
if v.flag&flagAddr != 0 {
c := unsafe_New(t)
typedmemmove(t, c, ptr)
ptr = c
}
e.word = ptr
case v.flag&flagIndir != 0:
e.word = *(*unsafe.Pointer)(v.ptr)
default:
e.word = v.ptr
}
e.typ = t
return i
// reflect.Value的Elem()方法仅对引用类型(Ptr和Interface{})有效,返回其引用的值
func (v Value) Elem() Value {
k := v.kind()
switch k {
case Interface:
var eface interface{}
if v.typ.NumMethod() == 0 {
eface = *(*interface{})(v.ptr)
} else {
eface = (interface{})(*(*interface {
M()
})(v.ptr))
}
x := unpackEface(eface)
if x.flag != 0 {
x.flag |= v.flag & flagRO
}
return x
case Ptr:
ptr := v.ptr
if v.flag&flagIndir != 0 {
ptr = *(*unsafe.Pointer)(ptr)
}
// The returned value's address is v's value.
if ptr == nil {
return Value{}
}
tt := (*ptrType)(unsafe.Pointer(v.typ))
typ := tt.elem
fl := v.flag&flagRO | flagIndir | flagAddr
fl |= flag(typ.Kind())
return Value{typ, ptr, fl}
}
panic(&ValueError{"reflect.Value.Elem", v.kind()})
}
参考:
-
Golang汇编快速指南
-
Go Interface源码剖析