Go学习值reflect反射篇

零、参考

一、反射

1. 介绍

简言之:就是利用【反射功能】注册【一些信息】到程序可执行文件中,因此在程序运行期间,可以利用【反射功能】取出或修改【这些信息】

反射是**【指在程序运行期间对程序本身进行访问和修改的能力】**。程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分。在运行程序时,程序无法获取自身的信息。

支持反射的语言可以在程序编译期间将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期间获取类型的反射信息,并且有能力修改它们。

2. 为什么要用反射

常用场景

需要反射的 2 个常见场景:

  1. 有时你需要编写一个函数,但是并不知道传给你的参数类型是什么,可能是没约定好;也可能是传入的类型很多,这些类型并不能统一表示。这时反射就会用的上了。
  2. 有时候需要根据某些条件决定调用哪个函数,比如根据用户的输入来决定。这时就需要对函数和函数的参数进行反射,在运行期间动态地执行函数。
不推荐使用的理由

在讲反射的原理以及如何用之前,还是说几点不使用反射的理由:

  1. 与反射相关的代码,经常是难以阅读的。在软件工程中,代码可读性也是一个非常重要的指标。
  2. Go 语言作为一门静态语言,编码过程中,编译器能提前发现一些类型错误,但是对于反射代码是无能为力的。所以包含反射相关的代码,很可能会运行很久,才会出错,这时候经常是直接 panic,可能会造成严重的后果。
  3. 反射对性能影响还是比较大的,比正常代码运行速度慢一到两个数量级。所以,对于一个项目中处于运行效率关键位置的代码,尽量避免使用反射特性。

3. 反射是如何实现的

当向【接口变量】赋予一个【实体类型】的时候,【接口会存储实体的类型信息】,反射就是【通过接口的类型信息实现的】,反射建立在类型的基础上。

Go 语言在 reflect 包里定义了各种类型,实现了反射的各种函数,通过它们可以在运行时检测类型的信息、改变类型的值。

  • 下面例子是,通过反射获取传入数据的类型
package main

import (
	"fmt"
	"reflect"
)

func reflectType(x interface{}) {
	v := reflect.TypeOf(x)
	fmt.Printf("type:%v\n", v)
}
func main() {
	var a float32 = 3.14
	reflectType(a) // type:float32
	var b int64 = 100
	reflectType(b) // type:int64
}

二、反射与接口的关系

1. interface 的底层结构

反射主要与 interface{} 类型相关。下面介绍一下 interface 的底层结构

type iface struct {
    tab  *itab
    data unsafe.Pointer
}

type itab struct {
    inter  *interfacetype
    _type  *_type
    link   *itab
    hash   uint32
    bad    bool
    inhash bool
    unused [2]byte
    fun    [1]uintptr
}

其中 itab 由具体类型 _type 以及 interfacetype 组成。_type 表示具体类型,而 interfacetype 则表示具体类型实现的接口类型。

实际上,iface 描述的是非空接口,它包含方法;与之相对的是 eface,描述的是空接口,不包含任何方法,Go 语言里有的类型都 “实现了” 空接口。

type eface struct {
    _type *_type
    data  unsafe.Pointer
}

相比 ifaceeface 就比较简单了。只维护了一个 _type 字段,表示空接口所承载的具体的实体类型。data 描述了具体的值。

2. interface结构与反射的关系

img
  1. ValueOf 函数返回值 reflect.Type 主要提供关于类型相关的信息,所以它和 _type 关联比较紧密;

  2. ValueOf 函数返回值 reflect.Value 表示 interface{} 里存储的实际变量,通过 Type() 方法和 Interface() 方法可以打通 interfaceTypeValue 三者。

    • Type() 方法也可以返回变量的类型信息,与 reflect.TypeOf() 函数等价。

    • Interface() 方法可以将 Value 还原成原来的 interface。

reflect 包里定义了一个接口和一个结构体,即 reflect.Typereflect.Value,它们提供很多函数来获取存储在接口里的类型信息。

  • reflect.Type 主要提供关于类型相关的信息,所以它和 _type 关联比较紧密;
  • reflect.Value 则结合 _typedata 两者,因此程序员可以获取甚至改变类型的值。

reflect 包中提供了两个基础的关于反射的函数来获取上述的接口和结构体:

func TypeOf(i interface{}) Type 
func ValueOf(i interface{}) Value
2.1 TypeOf

TypeOf 函数用来提取一个接口中值的类型信息。由于它的输入参数是一个空的 interface{},调用此函数时,实参会先被转化为 interface{} 类型。这样,实参的类型信息、方法集、值信息都存储到 interface{} 变量里了。

看下源码:

// 这里的 `emptyInterface` 和上面提到的 `eface` 是一回事(字段名略有差异,字段是相同的),
// 且在不同的源码包:前者在 `reflect` 包,后者在 `runtime` 包。 `eface.typ` 就是动态类型。
func TypeOf(i interface{}) Type {
    eface := *(*emptyInterface)(unsafe.Pointer(&i))
    return toType(eface.typ)
}

type emptyInterface struct {
    typ  *rtype
    word unsafe.Pointer
}

// 至于 `toType` 函数,只是做了一个类型转换:
func toType(t *rtype) Type {
    if t == nil {
        return nil
    }
    return t
}

// 注意,返回值 `Type` 实际上是一个接口,定义了很多方法,用来获取类型相关的各种信息,而 `*rtype` 实现了 `Type` 接口。
// Type 定义了非常多的方法,通过它们可以获取类型的一切信息,大家一定要完整的过一遍下面所有的方法。
type Type interface {
    // 所有的类型都可以调用下面这些函数
  
    // 此类型的变量对齐后所占用的字节数
    Align() int
    // 如果是 struct 的字段,对齐后占用的字节数
    FieldAlign() int
    // 返回类型方法集里的第 `i` (传入的参数)个方法
    Method(int) Method
    // 通过名称获取方法
    MethodByName(string) (Method, bool)
    // 获取类型方法集里导出的方法个数
    NumMethod() int
    // 类型名称
    Name() string
    // 返回类型所在的路径,如:encoding/base64
    PkgPath() string
    // 返回类型的大小,和 unsafe.Sizeof 功能类似
    Size() uintptr
    // 返回类型的字符串表示形式
    String() string
    // 返回类型的类型值 —— 底层类型
    Kind() Kind
    // 类型是否实现了接口 u
    Implements(u Type) bool
    // 是否可以赋值给 u
    AssignableTo(u Type) bool
    // 是否可以类型转换成 u
    ConvertibleTo(u Type) bool
    // 类型是否可以比较
    Comparable() bool
  
    // 下面这些函数只有特定类型可以调用
    // 如:Key, Elem 两个方法就只能是 Map 类型才能调用
  
    // 类型所占据的位数
    Bits() int
    // 返回通道的方向,只能是 chan 类型调用
    ChanDir() ChanDir
    // 返回类型是否是可变参数,只能是 func 类型调用
    // 比如 t 是类型 func(x int, y ... float64)
    // 那么 t.IsVariadic() == true
    IsVariadic() bool
    // 返回内部子元素类型,只能由类型 Array, Chan, Map, Ptr, or Slice 调用  
    // 这个很常用,比如变量 test 原类型为 []int , reflect.TypeOf(test).Elem() 会返回 int(即为子元素的类型)
    Elem() Type

    // 返回结构体类型的第 i 个字段,只能是结构体类型调用
    // 如果 i 超过了总字段数,就会 panic
    Field(i int) StructField
    // 返回嵌套的结构体的字段
    FieldByIndex(index []int) StructField
    // 通过字段名称获取字段
    FieldByName(name string) (StructField, bool)
    // FieldByNameFunc returns the struct field with a name
    // 返回名称符合 func 函数的字段
    FieldByNameFunc(match func(string) bool) (StructField, bool)
    // 获取函数类型的第 i 个参数的类型
    In(i int) Type
    // 返回 map 的 key 类型,只能由类型 map 调用
    Key() Type
    // 返回 Array 的长度,只能由类型 Array 调用
    Len() int
    // 返回类型字段的数量,只能由类型 Struct 调用
    NumField() int
    // 返回函数类型的输入参数个数
    NumIn() int
    // 返回函数类型的返回值个数
    NumOut() int
    // 返回函数类型的第 i 个值的类型
    Out(i int) Type
    // 返回类型结构体的相同部分
    common() *rtype
    // 返回类型结构体的不同部分
    uncommon() *uncommonType
}
2.2 ValueOf

讲完了 TypeOf 函数,再来看一下 ValueOf 函数。返回值 reflect.Value 表示 interface{} 里存储的实际变量,它能提供实际变量的各种信息。相关的方法常常是需要结合类型信息和值信息。例如,如果要提取一个结构体的字段信息,那就需要用到 _type (具体到这里是指 structType) 类型持有的关于结构体的字段信息、偏移信息,以及 *data 所指向的内容 —— 结构体的实际值。

源码如下:

func ValueOf(i interface{}) Value {
    if i == nil {
        return Value{}
    }

   // ……
    return unpackEface(i)
}

// 分解 eface
// 从源码看,比较简单:将先将 `i` 转换成 `*emptyInterface` 类型, 
// 再将它的 `typ` 字段和 `word` 字段以及一个标志位字段组装成一个 `Value` 结构体,而这就是 `ValueOf` 函数的返回值,
// 它包含类型结构体指针、真实数据的地址、标志位。
func unpackEface(i interface{}) 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}
}

Value 结构体定义了很多方法,通过这些方法可以直接操作 Value 字段 ptr 所指向的实际数据:

// 设置切片的 len 字段,如果类型不是切片,就会panic
 func (v Value) SetLen(n int)

 // 设置切片的 cap 字段
 func (v Value) SetCap(n int)

 // 设置字典的 kv
 func (v Value) SetMapIndex(key, val Value)

 // 返回切片、字符串、数组的索引 i 处的值
 func (v Value) Index(i int) Value

 // 根据名称获取结构体的内部字段值
 func (v Value) FieldByName(name string) Value

 // ……

Value 字段还有很多其他的方法。例如:

// 用来获取 int 类型的值
func (v Value) Int() int64

// 用来获取结构体字段(成员)数量
func (v Value) NumField() int

// 尝试向通道发送数据(不会阻塞)
func (v Value) TrySend(x reflect.Value) bool

// 通过参数列表 in 调用 v 值所代表的函数(或方法
func (v Value) Call(in []Value) (r []Value) 

// 调用变参长度可变的函数
func (v Value) CallSlice(in []Value) []Value

不一一列举了,反正是非常多。可以去 src/reflect/value.go 去看看源码,搜索 func (v Value) 就能看到。

另外,通过 Type() 方法和 Interface() 方法可以打通 interfaceTypeValue 三者。

  • Type() 方法也可以返回变量的类型信息,与 reflect.TypeOf() 函数等价。
  • Interface() 方法可以将 Value 还原成原来的 interface。
  • 见下图
img

三、golang 反射注意

  1. golang 反射不能获取和修改 私有(小写字母)的属性以及方法

  2. ValueOf(*ptr) 方法传递的参数必须是 指针类型 才可以修改字段否则会报错

四、Type 和 TypeOf

reflect.Type 类型是一个接口类型,内部指定了若干方法,通过这些方法我们可以获取到反射类型的各种信息,例如:字段、方法等

使用 *reflect.TypeOf()* 函数可以获取将任意值的类型对象 (reflect.Type),程序通过类型对象可以访问任意值的类型信息

1. 理解 Type 和 种类 Kind

reflect.Type 是变量的类型,而不是追根溯源的最底层类型

type MyInt int
reflect.TypeOf(MyInt).Kind()

这里的 reflect.Type 就是 MyInt,而非 int,如果想获得 int 只能使用Kind()

总结:*Type 表示的是静态类型,而 kind 表示的是底层真实的类型*

2. 获取类型名以及 kind

package main

import (
    "fmt"
    "reflect"
)

// 定义一个 MyInt 类型
type MyInt int

func main() {
    // 声明一个空结构体
    type cat struct {
    }
    // 获取结构体实例的反射类型对象
    typeOfCat := reflect.TypeOf(cat{})
    // 显示反射类型对象的名称和种类
    fmt.Println(typeOfCat.Name(), typeOfCat.Kind())
    // 获取Zero常量的反射类型对象
    typeOfA := reflect.TypeOf(Zero)
    // 显示反射类型对象的名称和种类
    fmt.Println(typeOfA.Name(), typeOfA.Kind())
}

代码输出如下:

cat struct

MyInt int

3. Type 常用方法

获取与成员相关的方法如下:

方法描述
Field(i int) StructField根据索引,返回索引对应的结构体字段的信息。当值不是结构体或索引超界时发生宕机
NumField() int返回结构体成员字段数量(包含私有字段)
FieldByName(name string) (StructField, bool)根据给定字符串返回字符串对应的结构体字段的信息。没有找到时 bool 返回 false
FieldByIndex(index []int) StructField多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的信息。没有找到时返回零值

StructField结构,这个结构描述结构体的成员信息,通过这个信息可以获取成员与结构体的关系,如偏移、索引、是否为匿名字段、结构体标签(Struct Tag)等

type StructField struct {
    Name      string    // 字段名
    PkgPath   string    // 字段路径
    Type      Type      // 字段反射类型对象
    Tag       StructTag // 字段的结构体标签
    Offset    uintptr   // 字段在结构体中的相对偏移
    Index     []int     // Type.FieldByIndex中的返回的索引值
    Anonymous bool      // 是否为匿名字段
}

4. 获取成员反射信息

package main
import (
    "fmt"
    "reflect"
)
func main() {
    // 声明一个空结构体
    type cat struct {
        Name string
        // 带有结构体tag的字段
        Type int `json:"type" id:"100"`
    }
    // 创建cat的实例
    ins := cat{Name: "mimi", Type: 1}
    // 获取结构体实例的反射类型对象
    typeOfCat := reflect.TypeOf(ins)
    // 遍历结构体所有成员
    for i := 0; i < typeOfCat.NumField(); i++ {
        // 获取每个成员的结构体字段类型
        fieldType := typeOfCat.Field(i)
        // 输出成员名和tag
        fmt.Printf("name: %v  tag: '%v'\n", fieldType.Name, fieldType.Tag)
    }
    // 通过字段名, 找到字段类型信息
    if catType, ok := typeOfCat.FieldByName("Type"); ok {
        // 从tag中取出需要的tag
        fmt.Println(catType.Tag.Get("json"), catType.Tag.Get("id"))
    }
}

5. 通过类型信息创建实例

当已知 reflect.Type 时,可以动态地创建这个类型的实例,实例的类型为指针

func main() {
    var a int
    // 取变量a的反射类型对象
    typeOfA := reflect.TypeOf(a)
    // 根据反射类型对象创建类型实例
    aIns := reflect.New(typeOfA)
    // 输出:*int ptr
    fmt.Println(aIns.Type(), aIns.Kind())
}

五、Value 和 ValueOf

reflect.Value 类型是一个结构体,封装了反射对象的值,内部若干方法,可以通过这些方法来获取和修改对象的值,使用 reflect.ValueOf 函数可以返回 Value 类型,value 类型还可以生成原始类型对象

1. 生成原始类型的对象

可以通过下面几种方法从反射值对象 reflect.Value 中获取原值

方法名说 明
Interface() interface {}将值以 interface{} 类型返回,可以通过类型断言转换为指定类型
Int() int64将值以 int 类型返回,所有有符号整型均可以此方式返回
Uint() uint64将值以 uint 类型返回,所有无符号整型均可以此方式返回
Float() float64将值以双精度(float64)类型返回,所有浮点数(float32、float64)均可以此方式返回
Bool() bool将值以 bool 类型返回
Bytes() []bytes将值以字节数组 []bytes 类型返回
String() string将值以字符串类型返回

代码演示如下👇

func main() {
    // 声明整型变量a并赋初值
    var a int = 1024
    // 获取变量a的反射值对象
    valueOfA := reflect.ValueOf(a)
    // 获取interface{}类型的值, 通过类型断言转换
    var getA int = valueOfA.Interface().(int)
    // 获取64位的值, 强制类型转换为int类型
    var getA2 int = int(valueOfA.Int())
    fmt.Println(getA, getA2)
}

2. 操作结构体成员的值

反射值对象(reflect.Value)提供对结构体访问的方法,通过这些方法可以完成对结构体任意值的访问,方法列表参考 Type 常用方法

修改成员的值 使用 reflect.Value 对包装的值进行修改时,需要遵循一些规则。如果该对象不可寻址或者成员是私有的,则无法修改对象值

判定是否可以操作的方法有如下👇

方法名描 述
Elem() Value取值指向的元素值,类似于语言层*操作。当值类型不是指针或接口时发生宕 机,空指针时返回 nil 的 Value
Addr() Value对可寻址的值返回其地址,类似于语言层&操作。当值不可寻址时发生宕机
CanAddr() bool表示值是否可寻址
CanSet() bool返回值能否被修改。要求值可寻址且是导出的字段

修改的方法如下👇

Set(x Value)将值设置为传入的反射值对象的值
Setlnt(x int64)使用 int64 设置值。当值的类型不是 int、int8、int16、 int32、int64 时会发生宕机
SetUint(x uint64)使用 uint64 设置值。当值的类型不是 uint、uint8、uint16、uint32、uint64 时会发生宕机
SetFloat(x float64)使用 float64 设置值。当值的类型不是 float32、float64 时会发生宕机
SetBool(x bool)使用 bool 设置值。当值的类型不是 bod 时会发生宕机
SetBytes(x []byte)设置字节数组 []bytes值。当值的类型不是 []byte 时会发生宕机
SetString(x string)设置字符串值。当值的类型不是 string 时会发生宕机

代码案例如下

func main() {
    type dog struct {
        LegCount int
        age int
    }
    // 获取dog实例地址的反射值对象
    valueOfDog := reflect.ValueOf(&dog{})
    // 取出dog实例地址的元素
    valueOfDog = valueOfDog.Elem()
    // 获取legCount字段的值
    vLegCount := valueOfDog.FieldByName("LegCount")
    vAge := valueOfDog.FieldByName("age")
    // 尝试设置legCount的值
    vLegCount.SetInt(4)
    // 这里会报错
    vAge.SetInt(4)
    fmt.Println(vLegCount.Int())
}

六、通过反射调用函数

如果反射值对象(reflect.Value)中值的类型为函数时,可以通过 reflect.Value 调用该函数,使用反射调用函数时,需要将参数使用反射值对象的切片 []reflect.Value 构造后传入 Call() 方法中,调用完成时,函数的返回值通过 []reflect.Value 返回

package main
import (
    "fmt"
    "reflect"
)
// 普通函数
func add(a, b int) int {
    return a + b
}
func main() {
    // 将函数包装为反射值对象
    funcValue := reflect.ValueOf(add)
    // 构造函数参数, 传入两个整型值
    paramList := []reflect.Value{reflect.ValueOf(10), reflect.ValueOf(20)}
    // 反射调用函数
    retList := funcValue.Call(paramList)
    // 获取第一个返回值, 取整数值
    fmt.Println(retList[0].Int())
}

七、通过反射调用对象中的方法

如果反射值对象中具有方法时,可以通过反射调用方法,获取方法如下👇

方法描述
Method(i int) Value根据索引,返回索引对应的方法
NumMethod() int返回结构体成员方法(包含私有)
MethodByName(name string) Value根据给定字符串返回字符串对应的结构体方法
package main

import (
    "fmt"
    "reflect"
)
type Cat struct {
    Name string
}

func (c *Cat) Sleep() {
    fmt.Println("呜呜呜...")
}

func main() {
    cat := Cat{}
    valueOf := reflect.ValueOf(&cat)
    showMethod := valueOf.MethodByName("Show")
    showMethod.Call(nil)
}

八、反射实现:map 转 struct

func Map2Struct(m map[string]interface{}, obj interface{}) {
    value := reflect.ValueOf(obj)

    // obj 必须是指针且指针指向的必须是 struct
    if value.Kind() == reflect.Ptr && value.Elem().Kind() == reflect.Struct {
        value = value.Elem()
        getMapName := func(key string) interface{} {
            for k, v := range m {
                if strings.EqualFold(k, key) {
                    return v
                }
            }
            return nil
        }
        // 循环赋值
        for i := 0; i < value.NumField(); i++ {
            // 获取字段 type 对象
            field := value.Field(i)
            if !field.CanSet() {
                continue
            }
            // 获取字段名称
            fieldName := value.Type().Field(i).Name
            fmt.Println("fieldName -> ", fieldName)
            // 获取 map 中的对应的值
            fieldVal := getMapName(fieldName)
            if fieldVal != nil {
                field.Set(reflect.ValueOf(fieldVal))
            }
        }
    } else {
        panic("must prt")
    }

}

九、反射实现:struct 转 map

func Struct2Map(obj interface{}) map[string]interface{} {
    value := reflect.ValueOf(obj)

    if value.Kind() != reflect.Ptr || value.Elem().Kind() != reflect.Struct {
        panic("must prt")
    }
    value = value.Elem()
    t := value.Type()

    // 创建 map
    resultMap := make(map[string]interface{})

    // 循环获取字段名称以及对应的值
    for i := 0; i < value.NumField(); i++ {
        val := value.Field(i)
        typeName := t.Field(i)
        if !val.CanSet() {
            resultMap[typeName.Name] = reflect.New(typeName.Type).Elem().Interface()
            continue
        }
        resultMap[typeName.Name] = val.Interface()
    }

    return resultMap
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值