关于go:go源码分析类型

31次阅读

共计 10869 个字符,预计需要花费 28 分钟才能阅读完成。

一 类型是怎么实现的

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  main
type User struct {
  Name string
  Age int
}

func (u User) GetName() string{return u.Name}
func (u User) GetAge() int {return u.Age}

他的类型构造如下图:

二 接口是怎么实现的

ifaceeface 都是 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 int
func 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 = b
c,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类型别离实现了 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 接口的办法。如下图所示

要确定 *Gopher 是否实现了 base 接口,同样会先去 itab 缓存里查找 <*Gopher,base> 对应的 itab,若存在,且itab.fun[0] 不等于 0,则断言胜利;若不存在,再去查看 *Gopher 的办法列表,创立并缓存 itab 信息。

综上,类型断言的要害是明确接口的动静类型,以及对应的类型实现了哪些办法。而明确这些的要害,还是类型元数据,以及空接口与非空接口的数据结构

四 反射是怎么实现的

用到反射的场景不外乎是变量类型不确定,内部结构不明朗的状况,所以反射的作用简略来说就是把类型元数据裸露给用户应用。

咱们曾经介绍过 runtime 包中 _type、uncommontype、eface、iface 等类型了,reflect也要和它们打交道,然而它们都属于未导出类型,所以 reflect 在本人的包中又定义了一套,两边的类型定义是保持一致的。

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函数会把 r untime.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}
}

能够看到其实也是取了空接口类型 _typedata

这里有一点能够留神,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
}

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

正文完
 0