go源码分析——类型

一 类型是怎样实现的 1、类型的基础类型 我们都知道怎么声明一个类型。例如type T struct { Name string} 。大家有没有思考过,当我们进行反射、接口动态派发、类型断言这些语言特性或机制,go语言是怎么识别这些类型的呢?其实编译器会给每种类型生成对应的类型描述信息写入可执行文件,这些类型描述信息就是“类型元数据”。
数据类型虽然很多,但是不管是内置类型还是自定义类型,它的“类型元数据”都是全局唯一的。这些类型元数据共同构成了Go语言的类型系统。如下图所示:
go源码分析——类型
文章图片

上面的类型都有一些公共的属性,像类型名称,大小,对齐边界,是否为自定义类型等信息,是每个类型元数据都要记录的。

type _type struct { sizeuintptr //数据类型占用的空间大小 ptrdatauintptr //含有所有指针类型前缀大小 hashuint32//类型hash值 tflagtflag//额外类型信息标志 alignuint8//该类型变量对齐方式 fieldAlign uint8//该类型结构字段对齐方式 kinduint8//类型编号 equal func(unsafe.Pointer, unsafe.Pointer) bool//判断对象是否相等 gcdata*byte//gc数据 strnameOff // 类型名字的偏移 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类型 hasherfunc(unsafe.Pointer, uintptr) uintptr //hash函数 keysizeuint8 elemsizeuint8 bucketsize uint16 flagsuint32 }

以上我们只是简单举两个例子,更多的内置类型的结构参考type.go
3、自定义类型 大家可能疑问,上面都是go语言里面的内置类型,那我们在代码中自己定义的类型是什么样的呢?
type.go文件还有一个uncommontype 类型:
type uncommontype struct { pkgpath nameOff mcountuint16 // 方法数量 xcountuint16 // 可导出的方法数量 moffuint32 // 记录的是这些方法的元数据组成的数组,相对于这个uncommontype结构体偏移了多少字节 _uint32 // unused }

moff标记了方法元数据的位置,方法的元数据的结构为:
type method struct { name nameOff mtyp typeOff ifntextOff tfntextOff }

可能上面这么讲还比较抽象,下面我们以一个自定义类型为例,来画图说明其数据结构:
例如我们自定义一个类型:
packagemain type User struct { Name string Age int }func (u User) GetName() string{ return u.Name } func (u User) GetAge() int { return u.Age }

他的类型结构如下图:
go源码分析——类型
文章图片

二 接口是怎样实现的 ifaceeface 都是 Go 中描述接口的底层结构体,区别在于 iface 描述的接口包含方法,而 eface 则是不包含任何方法的空接口:interface{}
1、空接口eface(interface{}) 我们先看一下eface的结构类型,可以看到eface 的结构非常简单,一个是我们上面提到的_type类型,标识数据的类型。data标识数据的具体位置
type eface struct { _type *_type dataunsafe.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的结构就如下图所示:
go源码分析——类型
文章图片

我们这个可以看到any的动态类型是*Gopher。这里提醒一下,类型元数据这里是可以找到类型关联的方法元数据列表的,这一点对于后面理解“类型断言”至关重要。
2、非空接口iface 同样,我们先来看一下非空接口的结构:
type iface struct { tab*itab //表示接口的类型以及赋给这个接口的实体类型 dataunsafe.Pointer //指向接口具体的值,一般而言是一个指向堆内存的指针 } type itab struct { inter*interfacetype _type*_type //实体结构体的类型 hashuint32 // _type的hash值 _[4]byte //内存对齐 fun[1]uintptr //存储的是第一个方法的函数指针,如果有更多的方法,在它之后的内存空间里继续存储,方法是按照函数名称的字典序进行排列的 } type interfacetype struct { typ_type //非空接口的类型 pkgpathname //包名 mhdr[]imethod //接口所定义的函数列表 }

可以用下面的图来描述一下上面源码的结构体类型:
go源码分析——类型
文章图片

我们再把上面的的示例代码修改一下:
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的内存结构是怎样的呢?下图展示了其结构:
go源码分析——类型
文章图片

itab的缓存 可能大家都有些疑问,我们每次进行any = g 类似的赋值的时候,那是不是每次都得吧itab初始化一下呢?其实itab也会有一个缓存,并且以<接口类型, 动态类型>组合为key,以*itab为value,构造一个哈希表,用于存储与查询itab信息。
//iface类型的缓存 //需要一个itab时,会首先去itabTable里查找,计算哈希值时会用到接口类型(itab.inter)和动态类型(itab._type)的类型哈希值: //如果能查询到对应的itab指针,就直接拿来使用。若没有就要再创建,然后添加到itabTable中。 type itabTableType struct { sizeuintptr// length of entries array. Always a power of 2. countuintptr// 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、怎么判断一个结构体实现了某个接口 【go源码分析——类型】通过前面提到的 iface 的源码可以看到,实际上它包含接口的类型 interfacetype 和 实体类型的类型 _type,这两者都是 iface 的字段 itab 的成员。也就是说生成一个 itab 同时需要接口的类型和实体的类型。
当判定一种类型是否满足某个接口时,Go 使用类型的方法集和接口所需要的方法集进行匹配,如果类型的方法集完全包含接口的方法集,则可认为该类型实现了该接口。
例如某类型有 m 个方法,某接口有 n 个方法,则很容易知道这种判定的时间复杂度为 O(mn),Go 会对方法集的函数按照函数名的字典序进行排序,所以实际的时间复杂度为 O(m+n)
三 断言 1、类型转换 在了解断言之前,我们先了解一下类型转换。
type MyInt int func main() { var i int = 9var 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 = b c,ok := a.(int8)

我们上面已经提到过,对于一个空接口其内部结构是这样的:
go源码分析——类型
文章图片

_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是否等于这个组合对应的itab指针就好。
go源码分析——类型
文章图片

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接口,如下图所示:
go源码分析——类型
文章图片

其实也并不需要每次都检查动态类型的方法列表,还记得itab缓存吗? 实际上,当类型断言的目标类型为非空接口时,会首先去itabTable里查找对应的itab指针,若没有找到,再去检查动态类型的方法列表。
此处注意,就算从itabTable中找到了itab指针,也要进一步确认itab.fun[0]是否等于0。这是因为一旦通过方法列表确定某个具体类型没有实现指定接口,就会把itab这里的fun[0]置为0,然后同样会把这个itab结构体缓存起来,和那些断言成功的itab缓存一样。这样做的目的是避免再遇到同种类型断言时重复检查方法列表
5、非空接口.(非空接口)断言 给出下面例子:*Gopher类型分别实现了basecoder接口:
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) }

anycoder接口类型,它是怎样断言成base接口类型的呢?其底层原理其实是判断any的动态类型*Gopher是否实现了base接口的方法。如下图所示
go源码分析——类型
文章图片

要确定*Gopher是否实现了base接口,同样会先去itab缓存里查找<*Gopher,base>对应的itab,若存在,且itab.fun[0]不等于0,则断言成功;若不存在,再去检查*Gopher的方法列表,创建并缓存itab信息。
综上,类型断言的关键是明确接口的动态类型,以及对应的类型实现了哪些方法。而明确这些的关键,还是类型元数据,以及空接口与非空接口的数据结构。
四 反射是怎样实现的 用到反射的场景不外乎是变量类型不确定,内部结构不明朗的情况,所以反射的作用简单来说就是把类型元数据暴露给用户使用。
我们已经介绍过runtime包中_type、uncommontype、eface、iface等类型了,reflect也要和它们打交道,但是它们都属于未导出类型,所以reflect在自己的包中又定义了一套,两边的类型定义是保持一致的。
go源码分析——类型
文章图片

reflect中有两个核心类型,reflect.Typereflect.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
go源码分析——类型
文章图片

因为*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} }

可以看到其实也是取了空接口类型_typedata
go源码分析——类型
文章图片

这里有一点可以注意,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 }

通过反射修改变量值的问题有点绕,但是只要记住函数传参值拷贝,以及反射修改变量值要作用到原变量身上才有意义这两个原则。

    推荐阅读