golang reflect

golang reflect实现原理

本文主要讲述reflect库实现的原理思路,reflect包实现具有两个基础unsafe操作内存对齐和runtime包的变量。

runtime变量

runtime变量是reflect的实现基础,基于unsafe包操作runtime变量实现reflect功能。

首先我们按照go的规则先简单的定义一个变量类型Value,Value有两个类型成员属性typ和ptr,typ是类型表示这个变量是什么对象,ptr是一个地址指向这个变量的地址。


type Value struct {
    typ Type
    ptr uintptr
}

type Type interface {
    Name() string         
    Index(int) Value      
    MapIndex(value) Value 
    Send(Value)           
}

当我们去操作一个变量时就按照Type类型来操作,而操作对象的数据就在内存的ptr位置。

变量类型Type定义的是一个接口,因为不同类型有不同的操作方法,例如Map的获取/设置值,Slice和Array的获取一个索引,Chan具有发送和接实一个对象,Struct可以获得一个结构体属性,属性具有tag,这样不同的类型就具有不同独特的操作方法,如果Map类型调用Index方法无法实现就会panic了。

理解变量本质就是一个数据地址和一个类型数据组成,然后基于者两个变量来操作就是reflect。

reflect example

一个reflect简单的例子,reflect.TypeOfreflect.ValueOf方法将一个runtime类型和变量转换成reflect类型和变量,依赖unsafe操作内存对齐来强制转换,reflect类型和变量和runtime中一样的,就可以实现自由操作了。

最后reflect.Value调用Interface()方法将变量从reflect状态转换回来成runtime状态了。

package main

import (
    "fmt"
    "reflect"
)

type Student struct {
    Name string
    Age  int
}

func main() {
    s := new(Student)
    fmt.Println(reflect.TypeOf(s))
    fmt.Println(reflect.TypeOf(s).Elem())
    fmt.Println(reflect.TypeOf(*s))

    v := reflect.ValueOf(s).Elem()
    v.Field(0).SetString("66")
    fmt.Printf("%#v\n", v.Interface())
}

reflect Type

先从reflect/type.go简单的抄一点代码来。rtype对象就是Type接口的简化实现,kind就是这个类型的类型,然后其他组合类型(Ptr、Slice、Map等)就额外添加了一些属性和方法。

type rtype struct {
    size    uintptr
    ptrdata uintptr
    kind    uint8
    ...
}

ptrType是指针类型的定义,属性rtype就是指针的类型,elem就是指针指向的类型,那么一个Ptr Type调用Elem获得指针的类型就返回了elem值。


type ptrType struct {
    rtype
    elem *rtype 
}

structType是指针类型的定义,rtype是结构体类型的基础信息,pkgPath就是结构体的名称,当一个结构体调用Name方法时就返回了pkgPath,如果是结构体指针调用Name方法就没有返回数据,因为没有pkgPath需要先Elem一次转换成结构体类型,而结构体类型的Field、FieldByIndex、FieldByName、FieldByNameFunc方法就对象结构体类型fields信息进行变量操作了。

而在结构体属性structField中,name、typ分别记录这个属性的名称和类型,offsetEmbed是属性偏移位置。


type structType struct {
    rtype
    pkgPath name
    fields  []structField 
}


type structField struct {
    name        name    
    typ         *rtype  
    offsetEmbed uintptr 
}

chanType是chan类型的ing有,rtype是chan本身,elem是chan操作对象的类型和指针指向相识,dir就是chan的反向进、出、进出。


type chanType struct {
    rtype
    elem *rtype  
    dir  uintptr 
}

sliceType是切片类型定义,切片类型rtype是本身信息,elem就是切片操作的对象类型。


type sliceType struct {
    rtype
    elem *rtype 
}

arrayType是数组类型,在切片上额外多了两个属性,slice是数组转换成切片的类型,预先静态定义好了,而len是数组长度。


type arrayType struct {
    rtype
    elem  *rtype 
    slice *rtype 
    len   uintptr
}

上述example讲述了部分类型的定义,完整查看源码reflect.type.go。

method、interface、map暂未完全看完,懂原理后没必要看没有几行使用相关知识。

reflect Kind

reflect.Kind是定义反射类型常量,是类型的标识。rtype的kind属性就是指reflect.Kind。



type Kind uint

const (
    Invalid Kind = iota
    Bool
    Int
    Int8
    Int16
    Int32
    Int64
    Uint
    Uint8
    Uint16
    Uint32
    Uint64
    Uintptr
    Float32
    Float64
    Complex64
    Complex128
    Array
    Chan
    Func
    Interface
    Map
    Ptr
    Slice
    String
    Struct
    UnsafePointer
)

reflect Type method

Kind方法注释说明返回kind值就是rtype.kind,类型是reflect.Kind是go中类型的主要分类,是iota定义的类型常量。


Kind() Kind

变量实现的方法定义在类型连续后面的一块内存中,可以unsafe读到一个类型的全部方法,就可以实现Implements方法判断是否实现了一个接口了。


Implements(u Type) bool

ChanDir方法很简单就返回chanType.dir,注释说如果不是Chan类型panic了,类型不chan就没有dir这个属性无法处理就panic了,在调用前一般都明确了Kind是Chan。



ChanDir() ChanDir

Elem方法全称是element,就是指元素类型也可以叫指向类型,注释要求Kind必须是Array、Chan、Map、Ptr、Slice类型否在就panic,和Chan的ChanDir方法一样,只有这5个类型才有elem属性。

查看前面定义就可以知道Arry、Slice、Ptr、Chan的elem就是指向的对象的类型,map是值的类型,例如以下类型Elem后Kind都是Int。

[20]int
[]int
*int
chan int
map[string]int


Elem() Type

Field和NumField方法是获得结构体的指定索引的属性和结构体属性数量,注释一样有说明要求Kind是Struct类型否在panic,因为就结构体类型才有[]StructField能实现这些方法。

根据前面structType定义两个方法的实现思路就是typ.fields[i]转换一下和len(typ.fields).




Field(i int) StructField


NumField() int

NumIn和In方法是Func Kind独有的方法,NumIn返回这个Func具有多个入参,对于返回参数就是NumOut;In方法是获得这个Func指定第i参数的类型。



NumIn() int
    


In(i int) Type

Key方法是Map Kind独有方法,返回map键的类型。




Key() Type

Len方法是Array Kind独有方法,返回Array定义的长度。



Len() int

上述说明reflect.Type的部分方法实现原理,剩余方法原理类似,就是操作rtype的属性,部分Kind类型是具有独有方法可以调用。

reflect.Value Method

反射Value对象定义了三个属性 类型、数据位置、flag,数据内存位置就在ptr位置,操作方法就需要依靠typ类型来判断数据类型操作了。

Type是静态数据,而Value是动态数据,Value的很多方法具体值是和数据相关的。

type Value struct {
    typ *rtype
    ptr unsafe.Pointer
    flag
}

通用方法

通用方法是指所有类型具有的方法,仅说明根据Type和Value定义实现这个方法大概的思路,具体实现代码并不一样,以源码为准

Type方法返回这个值的类型,大致思路就是返回v.typ,具体实现还有一些额外处理。

func (v Value) Type() Type

Kind方法实现大致思路就是返回v.typ.kind。



func (v Value) Kind() Kind

Interface 法思路就是返回v.ptr值转换成一个interface{}变量,这样就从reflect.Value重新转换成变量了。





func (v Value) Interface() (i interface{})

Convert 方法思路就是v.ptr值转换成参数t的类型,实现规则是Conversions语法文档 镜像地址


func (v Value) Convert(t Type) Value

Set 方法实现就是设置v.ptr=x.ptr,要求v和x的类型是一样的。

同时要求这个Value是CanSet,如果将一个int转换成reflect.Value,函数传递的是一个值的副本,那么再对int设置新的值就无效了,CanSet返回就是false,需要传递*int这样的指针类型才能有效设置


func (v Value) Set(x Value)

SetBool 方法是设置bool Kind的值,前置要求Kind是一样的,类型还有SetInt、SetString等方法。


func (v Value) SetBool(x bool)

Method 返回这个值的指定索引方法。





func (v Value) Method(i int) Value

独有方法

Len方法返回数据数量,相当于内建函数len(),注释说明要求是Array, Chan, Map, Slice, or String,前四个返回就是数据量,而String Kind返回字符串长度。


func (v Value) Len() int

IsNil方法判断指针是否是空,在go的实现中chan、func、interface、map、pointer、slice底层才是指针类型,才能判断IsNil否在panic,判断这些指针类型的ptr是否为0,在go代码编写中也只有这几种类型可以i==nil这样的比较。

在go1.13中新增了IsZero方法,判断是否是空值,里面这些指针类型会判断IsNil,其他类型就是判断数据值是不是零值那样。







func (v Value) IsNil() bool

Index方法获取指定类型的索引,就Array、Slice、String可以执行,否在panic,在ptr指向的位置进行一个计算得到的偏移位置获得到索引的值。


func (v Value) Index(i int) Value

Field方法是返回结构体指定索引的值,要求Kind是Struct,通过指定索引的偏移来获得这个值的地址,然后类型里面获得到类型,最后返回索引的值。


func (v Value) Field(i int) Value

Elem方法是返回Ptr和Interface Kind指向值,为了解除引用。

为什么Value.Elem方法没有了Slice、Map等类型? 具体额外独立的操作方法Index、MapIndex等。



func (v Value) Elem() Value

MapIndex和MapKeys是Map Kind独有的方法,获取到map的索引值和全部键,通过typ提供的类型和ptr地址进行复杂的map操作。





func (v Value) MapIndex(key Value) Value




func (v Value) MapKeys() []Value

Send方法是Chan Kind独有方法,给chan放一个数据进去。

func (v Value) Send(x Value)

end

以上讲述了reflect库的原理就是操作runtime变量,而runtime变量就是一个类型加地址。

本文并没有完整分析reflect库,通过这些原理就可以大概理解这些方法的作用和操作了,具体请参考源码。

Last updated

Was this helpful?