一 类型是怎么实现的
1、类型的根底类型
咱们都晓得怎么申明一个类型。例如type T struct { Name string}
。大家有没有思考过,当咱们进行反射、接口动静派发、类型断言这些语言个性或机制,go语言是怎么辨认这些类型的呢?其实编译器会给每种类型生成对应的类型形容信息写入可执行文件,这些类型形容信息就是“类型元数据”。
数据类型尽管很多,然而不论是内置类型还是自定义类型,它的“类型元数据”都是全局惟一的。这些类型元数据独特形成了Go语言的类型零碎。如下图所示:
下面的类型都有一些公共的属性,像类型名称,大小,对齐边界,是否为自定义类型等信息,是每个类型元数据都要记录的。
type _type struct { size uintptr //数据类型占用的空间大小 ptrdata uintptr //含有所有指针类型前缀大小 hash uint32 //类型hash值 tflag tflag //额定类型信息标记 align uint8 //该类型变量对齐形式 fieldAlign uint8 //该类型构造字段对齐形式 kind uint8 //类型编号 equal func(unsafe.Pointer, unsafe.Pointer) bool//判断对象是否相等 gcdata *byte //gc数据 str nameOff // 类型名字的偏移 ptrToThis typeOff}
对于具体的类型,他们的元数据是怎么存储的呢?咱们分为内置类型和自定义类型来别离剖析。
2、内置类型
对于内置类型,大部分也都在runtime.type
文件外面。
咱们先看看切片:elem
是存储切片内元素的类型,比方:如果是 []string
,那么elem就是stringtype
type slicetype struct { typ _type elem *_type //切片内元素的类型}
再看看咱们上文提到的map类型:能够看到它记录了 key、value、bucket的类型和大小
type maptype struct { typ _type key *_type //key类型 elem *_type //value类型 bucket *_type // bucket类型 hasher func(unsafe.Pointer, uintptr) uintptr //hash函数 keysize uint8 elemsize uint8 bucketsize uint16 flags uint32}
以上咱们只是简略举两个例子,更多的内置类型的构造参考type.go
3、自定义类型
大家可能疑难,下面都是go语言外面的内置类型,那咱们在代码中本人定义的类型是什么样的呢?
type.go文件还有一个uncommontype
类型:
type uncommontype struct { pkgpath nameOff mcount uint16 // 办法数量 xcount uint16 // 可导出的办法数量 moff uint32 // 记录的是这些办法的元数据组成的数组,绝对于这个uncommontype构造体偏移了多少字节 _ uint32 // unused}
moff
标记了办法元数据的地位,办法的元数据的构造为:
type method struct { name nameOff mtyp typeOff ifn textOff tfn textOff}
可能下面这么讲还比拟形象,上面咱们以一个自定义类型为例,来画图阐明其数据结构:
例如咱们自定义一个类型:
package maintype User struct { Name string Age int}func (u User) GetName() string{ return u.Name}func (u User) GetAge() int { return u.Age}
他的类型构造如下图:
二 接口是怎么实现的
iface
和 eface
都是 Go 中形容接口的底层构造体,区别在于 iface
形容的接口蕴含办法,而 eface
则是不蕴含任何办法的空接口:interface{}
1、空接口eface(interface{})
咱们先看一下eface
的构造类型,能够看到eface
的构造非常简单,一个是咱们下面提到的_type
类型,标识数据的类型。data
标识数据的具体位置
type eface struct { _type *_type data unsafe.Pointer}
咱们还是举个例子:
func main() { var any interface{} g := &Gopher{"Go"} any = g}type Gopher struct { language string}func (p *Gopher) code() { fmt.Printf("I am coding %s language\n", p.language)}func (p *Gopher) debug() { fmt.Printf("I am debuging %s language\n", p.language)}
咱们把Gopher
类型的变量g
赋给any
。那么变量any
的构造就如下图所示:
咱们这个能够看到any的动静类型是*Gopher。这里揭示一下,类型元数据这里是能够找到类型关联的办法元数据列表的,这一点对于前面了解“类型断言”至关重要。
2、非空接口iface
同样,咱们先来看一下非空接口的构造:
type iface struct { tab *itab //示意接口的类型以及赋给这个接口的实体类型 data unsafe.Pointer //指向接口具体的值,一般而言是一个指向堆内存的指针}type itab struct { inter *interfacetype _type *_type //实体构造体的类型 hash uint32 // _type的hash值 _ [4]byte //内存对齐 fun [1]uintptr //存储的是第一个办法的函数指针,如果有更多的办法,在它之后的内存空间里持续存储,办法是依照函数名称的字典序进行排列的}type interfacetype struct { typ _type //非空接口的类型 pkgpath name //包名 mhdr []imethod //接口所定义的函数列表}
能够用上面的图来形容一下下面源码的构造体类型:
咱们再把下面的的示例代码批改一下:
func main() { var any coder g := &Gopher{"Go"} any = g}type coder interface { code() debug()}type Gopher struct { language string}func (p *Gopher) code() { fmt.Printf("I am coding %s language\n", p.language)}func (p *Gopher) debug() { fmt.Printf("I am debuging %s language\n", p.language)}
咱们把*Gopher
类型复制给coder
接口类型的any
,那么any的内存构造是怎么的呢?下图展现了其构造:
itab的缓存
可能大家都有些疑难,咱们每次进行any = g
相似的赋值的时候,那是不是每次都得吧itab初始化一下呢?其实itab也会有一个缓存,并且以<接口类型, 动静类型>组合为key,以*itab
为value,结构一个哈希表,用于存储与查问itab信息。
//iface类型的缓存// 须要一个itab时,会首先去itabTable里查找,计算哈希值时会用到接口类型(itab.inter)和动静类型(itab._type)的类型哈希值:// 如果能查问到对应的itab指针,就间接拿来应用。若没有就要再创立,而后增加到itabTable中。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}//hash函数 接口类型&动静类型func itabHashFunc(inter *interfacetype, typ *_type) uintptr { return uintptr(inter.typ.hash ^ typ.hash)}
所以须要一个itab的时候,会先去itabTableType.entries去找到有没有对应的*itab,没有则会初始化一个。
3、怎么判断一个构造体实现了某个接口
通过后面提到的 iface
的源码能够看到,实际上它蕴含接口的类型 interfacetype
和 实体类型的类型 _type
,这两者都是 iface
的字段 itab
的成员。也就是说生成一个 itab
同时须要接口的类型和实体的类型。
当断定一种类型是否满足某个接口时,Go 应用类型的办法集和接口所须要的办法集进行匹配,如果类型的办法集齐全蕴含接口的办法集,则可认为该类型实现了该接口。
例如某类型有 m
个办法,某接口有 n
个办法,则很容易晓得这种断定的工夫复杂度为 O(mn)
,Go 会对办法集的函数依照函数名的字典序进行排序,所以理论的工夫复杂度为 O(m+n)
。
三 断言
1、类型转换
在理解断言之前,咱们先理解一下类型转换。
type MyInt intfunc main() { var i int = 9 var f float64 f = float64(i) f = 10.8 a := int(f) // s := []int(i) myInt := MyInt(a)}
下面的代码里,我定义了一个 int
型和 float64
型的变量,尝试在它们之前互相转换,后果是胜利的:int
型和 float64
是互相兼容的。
如果我把s := []int(i)
正文去掉,编译器会报告类型不兼容的谬误,因为其底层类型不兼容。
因为MyInt
底层类型为int
,所以myInt := MyInt(a)
也会兼容
所以:只有当底层类型能够互相转换的时候能力进行类型转化
2、空接口.(具体类型)断言
咱们看看上面的断言产生了什么呢?
var a interface{}b := int8(1)a = bc,ok := a.(int8)
咱们下面曾经提到过,对于一个空接口其内部结构是这样的:
_type
会指向int8类型元数据,所以当断言的时候,咱们之前介绍过,类型的元数据是惟一的,只须要比拟 _type
的元数据类型和int8
的元数据类型是否相等,就能够断言胜利
3、非空接口.(具体类型)断言
先拿出咱们之前的例子:
func main() { var any coder g := &Gopher{"Go"} any = g newG,ok := any.(*Gopher)}type coder interface { code() debug()}type Gopher struct { language string}func (p *Gopher) code() { fmt.Printf("I am coding %s language\n", p.language)}func (p *Gopher) debug() { fmt.Printf("I am debuging %s language\n", p.language)}
newG,ok := any.(*Gopher)
是要判断coder
的动静类型是否为*Gopher
类型。后面咱们介绍过,程序中用到的itab构造体都会缓存起来,能够通过<接口类型, 动静类型>组合起来的key,查找到对应的itab指针。所以这里的类型断言只须要一次比拟就能实现,就是看iface.tab
是否等于<coder, *Gopher>
这个组合对应的itab指针就好。
4、空接口.(非空接口)断言
func main() { var any interface{} g := &Gopher{"Go"} any = g newG,ok := any.(coder)}type coder interface { code() debug()}type Gopher struct { language string}func (p *Gopher) code() { fmt.Printf("I am coding %s language\n", p.language)}func (p *Gopher) debug() { fmt.Printf("I am debuging %s language\n", p.language)}
newG,ok := any.(coder)
判断interface{}
空接口是否是coder
接口类型。any
的动静类型就是*Gopher
,咱们晓得*Gopher
类型元数据的前面能够找到该类型实现的办法列表形容信息。找到其办法后就能够确定是否实现了coder
接口,如下图所示:
其实也并不需要每次都查看动静类型的办法列表,还记得itab
缓存吗? 实际上,当类型断言的指标类型为非空接口时,会首先去itabTable
里查找对应的itab
指针,若没有找到,再去查看动静类型的办法列表。
此处留神,就算从itabTable
中找到了itab
指针,也要进一步确认itab.fun[0]
是否等于0。这是因为一旦通过办法列表确定某个具体类型没有实现指定接口,就会把itab这里的fun[0]
置为0,而后同样会把这个itab构造体缓存起来,和那些断言胜利的itab缓存一样。这样做的目标是防止再遇到同种类型断言时反复查看办法列表
5、非空接口.(非空接口)断言
给出上面例子:*Gopher
类型别离实现了base
和coder
接口:
func main() { var any coder g := &Gopher{"Go"} any = g b, ok := any.(base)}type base interface { say()}type coder interface { code() debug()}type Gopher struct { language string}func (p *Gopher) code() { fmt.Printf("I am coding %s language\n", p.language)}func (p *Gopher) debug() { fmt.Printf("I am debuging %s language\n", p.language)}func (p *Gopher) say() { fmt.Printf("I am say %s language\n", p.language)}
那any
是coder
接口类型,它是怎么断言成base
接口类型的呢?其底层原理其实是判断any
的动静类型*Gopher
是否实现了base
接口的办法。如下图所示
要确定*Gopher
是否实现了base
接口,同样会先去itab
缓存里查找<*Gopher,base>
对应的itab
,若存在,且itab.fun[0]
不等于0,则断言胜利;若不存在,再去查看*Gopher
的办法列表,创立并缓存itab
信息。
综上,类型断言的要害是明确接口的动静类型,以及对应的类型实现了哪些办法。而明确这些的要害,还是类型元数据,以及空接口与非空接口的数据结构。
四 反射是怎么实现的
用到反射的场景不外乎是变量类型不确定,内部结构不明朗的状况,所以反射的作用简略来说就是把类型元数据裸露给用户应用。
咱们曾经介绍过runtime
包中_type、uncommontype、eface、iface
等类型了,reflect
也要和它们打交道,然而它们都属于未导出类型,所以reflect在本人的包中又定义了一套,两边的类型定义是保持一致的。
reflect中有两个外围类型,reflect.Type
和reflect.Value
,它们两个撑起了反射性能的根本框架。
1、reflect.Type
reflect.Type
是一个接口类型,它定义了一系列办法用于获取类型各方面的信息
type Type interface { Align() int //对齐边界 FieldAlign() int //作为构造体字段的对齐边界 Method(int) Method //获取办法数组中第i个Method(只会获取可导出的办法,办法依照字典序排序) MethodByName(string) (Method, bool) //依照名称查找办法 NumMethod() int //办法列表中可导出办法的数目 Name() string //类型名称 PkgPath() string //包门路 Size() uintptr //该类型变量占用字节数 String() string //获取类型的字符串示意 Kind() Kind //类型对应的reflect.Kind Implements(u Type) bool //该类型是否实现了接口u AssignableTo(u Type) bool //是否能够赋值给类型u ConvertibleTo(u Type) bool //是否可转换为类型u Comparable() bool //是否可比拟 //返回类型的大小(以位为单位) //只能利用于某些Kind的办法 //Int*, Uint*, Float*, Complex*: Bits() int ChanDir() ChanDir //返回通道的方向 IsVariadic() bool //办法的最初一个参数是否是可变参数(相似于...string) Elem() Type //Array, Chan, Map, Pointer, or Slice的参数类型 Field(i int) StructField //返回构造体的第i个属性 FieldByIndex(index []int) StructField //逐级查找构造体的属性相似于;A.B.C FieldByName(name string) (StructField, bool)//依据名字查找构造体的属性 FieldByNameFunc(match func(string) bool) (StructField, bool)//依据名字查找构造体的属性(查找的办法自定义) In(i int) Type//返回办法的第i个入参 Key() Type //返回map的key类型 Len() int //返回数组的长度 NumField() int //返回构造体属性的个数 NumIn() int //返回办法入参的个数 NumOut() int//返回办法出参的个数 Out(i int) Type//办法第i哥出参 common() *rtype uncommon() *uncommonType}
通常会用reflect.TypeOf
这个函数来拿到一个reflect.Type
类型的返回值。
func TypeOf(i interface{}) Type { eface := *(*emptyInterface)(unsafe.Pointer(&i)) return toType(eface.typ)}// emptyInterface is the header for an interface{} value.type emptyInterface struct { typ *rtype word unsafe.Pointer}
它接管一个空接口类型的参数,reflect.TypeOf
函数会把runtime.eface
类型的参数i
转换成reflect.emptyInterface类型并赋给局部变量eface
因为*rtype
实现了reflect.Type
接口,所以只有把eface
这里的typ
字段取出来,包装成reflect.Type
类型的返回值就好了。这就相当于上面这样把eface.typ
赋值给一个reflect.Type
类型的变量。
至于*rtype
实现的这些接口要求的办法,也总不过是去type
字段指向的类型元数据那里获取各种信息罢了。
咱们以Implements
办法为例,要判断t
是否实现了u
,须要把t
的所有办法取出来和u
的办法做比拟,如果t
的办法能全副匹配到u
的办法,则返回true
func (t *rtype) Implements(u Type) bool { if u == nil { panic("reflect: nil type passed to Type.Implements") } if u.Kind() != Interface { panic("reflect: non-interface type passed to Type.Implements") } return implements(u.(*rtype), t)}func implements(T, V *rtype) bool { if T.Kind() != Interface { return false } t := (*interfaceType)(unsafe.Pointer(T)) if len(t.methods) == 0 { //空接口 return true } //如果V是接口 //循环比拟V中的办法是否和T中的办法匹配,如果全匹配,返回true //i示意T接口第i哥办法 j示意V接口第j哥办法 if V.Kind() == Interface { v := (*interfaceType)(unsafe.Pointer(V)) i := 0 for j := 0; j < len(v.methods); j++ { tm := &t.methods[i] tmName := t.nameOff(tm.name) vm := &v.methods[j] vmName := V.nameOff(vm.name) if vmName.name() == tmName.name() && V.typeOff(vm.typ) == t.typeOff(tm.typ) {//办法是否相等 //因为办法曾经依照字典序排序,所以当i是T接口最初一个办法的时候, //证实T接口所有的办法在V中都找到对应的办法 if i++; i >= len(t.methods) { return true } } } return false } //V是非接口,比拟办法和下面一样,只是取办法的形式不一样 v := V.uncommon() if v == nil { return false } i := 0 vmethods := v.methods() for j := 0; j < int(v.mcount); j++ { tm := &t.methods[i] tmName := t.nameOff(tm.name) vm := vmethods[j] vmName := V.nameOff(vm.name) if vmName.name() == tmName.name() && V.typeOff(vm.mtyp) == t.typeOff(tm.typ) { if i++; i >= len(t.methods) { return true } } } return false}
2、reflect.Value
与reflect.Type
不同,reflect.Value
是一个构造体类型
type Value struct { typ *rtype //类型元数据 ptr unsafe.Pointer //存储数据地址 flag //一个位标识符,存储反射变量值的一些形容信息,例如类型掩码,是否为指针,是否为办法,是否只读等等}type flag uintptr
reflect.ValueOf
函数的参数也是空接口类型
func ValueOf(i interface{}) Value { if i == nil { return Value{} } escapes(i) return unpackEface(i)}func unpackEface(i any) Value { e := (*emptyInterface)(unsafe.Pointer(&i)) t := e.typ if t == nil { return Value{} } f := flag(t.Kind()) if ifaceIndir(t) { f |= flagIndir } return Value{t, e.word, f}}
能够看到其实也是取了空接口类型_type
和data
这里有一点能够留神,reflect.ValueOf
函数目前的实现形式,会通过escapes
函数显示地把参数i
指向的变量逃逸到堆上。
咱们以上面的例子剖析,发现会panic
func main() { a := "peacexu" v := reflect.ValueOf(a) v.SetString("new pecexu") fmt.Println(a) //panic: reflect: reflect.Value.SetString using unaddressable value}
咱们来看看SetString
的源码,会发现如果传入v := reflect.ValueOf(a)
的a
不是指针类型,就会产生panic。
// SetString sets v's underlying value to x.// It panics if v's Kind is not String or if CanSet() is false.func (v Value) SetString(x string) { v.mustBeAssignable() v.mustBe(String) *(*string)(v.ptr) = x}func (f flag) mustBeAssignable() { if f&flagRO != 0 || f&flagAddr == 0 { f.mustBeAssignableSlow() }}func (f flag) mustBeAssignableSlow() { if f == 0 { panic(&ValueError{methodNameSkip(), Invalid}) } // Assignable if addressable and not read-only. if f&flagRO != 0 { panic("reflect: " + methodNameSkip() + " using value obtained using unexported field") } if f&flagAddr == 0 {//如果不是指针类型 panic panic("reflect: " + methodNameSkip() + " using unaddressable value") }}
为什么要这么设计呢?咱们晓得办法传参都是值传递,咱们传递了一份string(a)
类型,Valueof
办法承受到的其实是a的一份正本,那么批改a的正本将没有任何意义,所以此处会panic。
接下来咱们改成上面这样:
func main() { a := "peacexu" v := reflect.ValueOf(&a) v.SetString("new pecexu") fmt.Println(a)//panic: reflect: reflect.Value.SetString using unaddressable value}
咱们发现还是会panic。为什么呢?因为&a
尽管是指针类型,然而传递过来的依然是指针的一份正本,所以SetString
是扭转的指针的正本,进而panic
所以咱们须要拿到指针对应的值,再进行批改就没问题了
func main() { a := "peacexu" v := reflect.ValueOf(&a) v.Elem().SetString("new pecexu") fmt.Println(a)//new pecexu}
通过反射批改变量值的问题有点绕,然而只有记住函数传参值拷贝,以及反射批改变量值要作用到原变量身上才有意义这两个准则。