LOADING

正在加载

Go基础学习(拾贰)——反射

壹 反射介绍

根据前面学习我们可以通过fmt包和类型断言进行识别判断数据的类型和值,但是fmt包只能是直观的查看输出,并不能在程序运行时让计算机了解识别数据,而断言类型只是针对接口的,对于其他类型就爱莫能助了,这时候我们就需要引入反射,况且fmt包本质也是使用的反射,获取数据类型和值。

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

  • 小插曲——变量的内在机制
  • Go语言中的变量是分为两部分的:
    • 类型信息:预先定义好的元信息。
    • 值信息:程序运行过程中可动态变化的。

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

空接口可以存储任意类型的变量,那我们如何知道这个空接口保存的数据是什么呢? 反射就是在运行时动态的获取一个变量的类型信息和值信息。反射就是在运行时动态的获取一个变量的类型信息和值信息。

贰 reflect包

在Go语言的反射机制中,任何接口值都由是一个具体类型具体类型的值两部分组成的。 在Go语言中反射的相关功能由内置的reflect包提供,任意接口值在反射中都可以理解为由reflect.Typereflect.Value两部分组成,并且reflect包提供了reflect.TypeOfreflect.ValueOf两个函数来获取任意对象的Value和Type。

叁 reflect.TypeOf——查看类型

3.1 TypeOf函数介绍

在Go语言中,使用reflect.TypeOf()函数可以获得任意值的类型对象(reflect.Type),程序通过类型对象可以访问任意值的类型信息。

func main() {
    var a float32 = 3.14
    fmt.Printf("value:%v\ttype:%T\n", reflect.TypeOf(a), reflect.TypeOf(a)) // value:float32  type:*reflect.rtype
    var b int64 = 100
    fmt.Printf("value:%v\ttype:%T\n", reflect.TypeOf(b), reflect.TypeOf(b)) // value:int64    type:*reflect.rtype
}

可以看到使用reflect.TypeOf()的值是对应的类型,其类型是*reflect.rtype

3.2 Name函数和Kind函数

在反射中关于类型还划分为两种:类型(Type)种类(Kind)。因为在Go语言中我们可以使用type关键字构造很多自定义类型,而种类(Kind)就是指底层的类型,但在反射中,当需要区分指针、结构体等大品种的类型时,就会用到种类(Kind)。 举个例子,我们定义了两个指针类型和两个结构体类型,通过反射查看它们的类型和种类。

package main

import (
    "fmt"
    "reflect"
)

type myInt int64

// 创建一个函数方便输出,因为空接口可以接收任意值,所以我们使用空接口做参数
func reflectType(x interface{}) {
    t := reflect.TypeOf(x)
    fmt.Printf("Name函数:type=%T\tvalue=%v\t", t.Name(), t.Name())
    fmt.Printf("Kind函数:type=%T\tvalue=%v\n", t.Kind(), t.Kind())
}

func main() {
    var a *float32 // 指针
    var b myInt    // 自定义类型
    var c rune     // 类型别名
    reflectType(a) // Name函数:type=string   value=   		   Kind函数:type=reflect.Kind     value=ptr
    reflectType(b) // Name函数:type=string   value=myInt      Kind函数:type=reflect.Kind     value=int64
    reflectType(c) // Name函数:type=string   value=int32      Kind函数:type=reflect.Kind     value=int32

    type person struct {
        name string
        age  int
    }
    type book struct{ title string }
    var d = person{
        name: "A7cc",
        age:  18,
    }
    var e = book{title: "一只特立独行的猪"}
    reflectType(d) // Name函数:type=string   value=person     Kind函数:type=reflect.Kind     value=struct
    reflectType(e) // Name函数:type=string   value=book       Kind函数:type=reflect.Kind     value=struct
}

可以看到Name函数返回的是字符串类型,值是当前类型,而Kind函数返回的reflect.Kind类型,值是底层类型。

Go语言的反射中像数组、切片、Map、指针等类型的变量,它们的.Name()都是返回,个人觉得他们本质就是指针,指针本身不属于基本类型,而是一种地址类型。

reflect包中定义的Kind类型如下:

type Kind uint

const (
    Invalid       Kind = iota // 非法类型
    Bool                      // 布尔型
    Int                       // 有符号整型
    Int8                      // 有符号8位整型
    Int16                     // 有符号16位整型
    Int32                     // 有符号32位整型
    Int64                     // 有符号64位整型
    Uint                      // 无符号整型
    Uint8                     // 无符号8位整型
    Uint16                    // 无符号16位整型
    Uint32                    // 无符号32位整型
    Uint64                    // 无符号64位整型
    Uintptr                   // 指针
    Float32                   // 单精度浮点数
    Float64                   // 双精度浮点数
    Complex64                 // 64位复数类型
    Complex128                // 128位复数类型
    Array                     // 数组
    Chan                      // 通道
    Func                      // 函数
    Interface                 // 接口
    Map                       // 映射
    Ptr                       // 指针
    Slice                     // 切片
    String                    // 字符串
    Struct                    // 结构体
    UnsafePointer             // 底层指针
)

肆 reflect.ValueOf——查看值

4.1 ValueOf函数介绍

reflect.ValueOf()返回的是reflect.Value类型,其中包含了原始值的值信息。reflect.Value与原始值之间可以互相转换。

reflect.Value类型提供的获取原始值的方法如下:

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

4.3 通过反射获取值

func reflectValue(x interface{}) {
    v := reflect.ValueOf(x)
    // ValueOf只能使用Kind函数获取种类,不能使用Name函数获取类型
    // 其实我们可以联想下,如果可以使用Name函数,那么对于指针、切片、map,返回的就是一个空,就不能判断类型了
    k := v.Kind()
    //在这里我们需要使用reflect类型判断,即:reflect.Kind类型,而不是简单的int、float32等判断
    switch k {
    // 这里的reflect是在`reflect`包中定义的Kind类型
    case reflect.Int64:
        // v.Int()从反射中获取整型的原始值,然后通过int64()强制类型转换
        fmt.Printf("type:%T, value:%v\n", v.Int(), v.Int())
    case reflect.Float32:
        // v.Float()从反射中获取浮点型的原始值,然后通过float32()强制类型转换
        fmt.Printf("type:%T, value:%v\n", v.Float(), v.Float())
    case reflect.Map:
        // v.Float()从反射中获取浮点型的原始值,然后通过float64()强制类型转换
        fmt.Printf("type:%T, value:%v\n", v.Float(), v.Float())
    }
}
func main() {
    var a float32 = 3.14
    var b int64 = 100
    reflectValue(a) // type:float32, value:3.140000
    reflectValue(b) // type:int64, value:100
    // 将int类型的原始值转换为reflect.Value类型
    c := reflect.ValueOf(10)
    // 需要区别这里的类型是返回直接使用了reflect.ValueOf的类型,而不是上面reflectValue函数使用了reflect.ValueOf再获取原始值的类型
    fmt.Printf("关于reflect.ValueOf类型:type=%T, value=%v\n", c, c) // 关于reflect.ValueOf类型:type=reflect.Value, value=10
}

使用reflect.ValueOf函数返回的值的类型是reflect.Value,而使用reflect.Value类型方法返回的类型是对应的基础类型。

4.4 通过反射设置变量的值——Elem函数

想要在函数中通过反射修改变量的值,需要注意函数参数传递的是值拷贝,必须传递变量地址才能修改变量值。而反射中使用专有的Elem()方法来获取指针对应的值,使用这个函数需要的是reflect.ValueOf类型。

package main

import (
    "fmt"
    "reflect"
)

func reflectSetValue1(x interface{}) {
    v := reflect.ValueOf(x)
    if v.Kind() == reflect.Int64 {
        v.SetInt(200) //修改的是副本,reflect包会引发panic,个人理解是SetInt内部实现需要的是一个指针,不是一个确切的值
    }
}
func reflectSetValue2(x interface{}) {
    v := reflect.ValueOf(x)
    // 反射中使用 Elem()方法获取指针对应的值
    if v.Elem().Kind() == reflect.Int64 {
        v.Elem().SetInt(200)
    }
}
func main() {
    var a int64 = 100
    // reflectSetValue1(a) //panic: reflect: reflect.Value.SetInt using unaddressable value
    reflectSetValue2(&a)
    fmt.Println(a)
}

4.5 isNil函数和isValid函数

在使用这两个函数时都需要是reflect.ValueOf类型。

4.5.1 isNil函数

func (v Value) IsNil() bool

IsNil()是判断v这个变量持有的值是不是为nil,而不是0,一般用来判断一个指针、切片等是否初始化,所以v持有的值的分类必须是通道、函数、接口、映射、指针、切片之一;否则IsNil函数会导致panic。IsNil()常被用于判断指针是否为空。

// 例子
func main() {
    // *int类型空指针
    var a *int
    fmt.Println("a指针的值是否是为空:", reflect.ValueOf(a).IsNil())	// a指针的值是否是为空: true
    // 不能为基础类型,同时可以看到使用反射时,再不运行的情况下,不会轻易报错,当运行才会报错
    // var b int
    // fmt.Println("b指针的值是否是为空:", reflect.ValueOf(b).IsNil())
}

4.5.2 isValid函数

func (v Value) IsValid() bool

IsValid()是判断v是否持有一个值,与isNil函数不同,同时持有不是判断值是否为nil。如果v是Value零值会返回假,此时v除了IsValid、String、Kind之外的方法都会导致panic。IsValid()常被用于判定返回值是否有效。

// 例子
func main() {
    // nil值
    fmt.Println("nil IsValid:", reflect.ValueOf(nil).IsValid()) // nil IsValid: false
    // 实例化一个匿名结构体
    b := struct{}{}
    // 尝试从结构体中查找"abc"字段
    // FieldByName方法是根据给定字符串返回字符串对应的结构体字段的信息,下面的结构体反射会讲解
    fmt.Println("不存在的结构体成员:", reflect.ValueOf(b).FieldByName("abc").IsValid()) // 不存在的结构体成员: false
    // 尝试从结构体中查找"abc"方法
    // MethodByName方法是根据方法名返回该类型方法集中的方法,下面的结构体反射会讲解
    fmt.Println("不存在的结构体方法:", reflect.ValueOf(b).MethodByName("abc").IsValid()) // 不存在的结构体方法: false

    // map
    c := map[string]int{}
    // 尝试从map中查找一个不存在的键
    fmt.Println("map中不存在的键:", reflect.ValueOf(c).MapIndex(reflect.ValueOf("abc")).IsValid()) // map中不存在的键: false
}

伍 结构体反射

5.1 StructField类型

StructField类型用来描述结构体中的一个字段的信息,该类型是描述结构体中非常重要的类型,StructField的定义如下:

type StructField struct {
    // Name是字段的名字。PkgPath是非导出字段的包路径,对导出字段该字段为""。
    // 参见http://golang.org/ref/spec#Uniqueness_of_identifiers
    Name      string
    PkgPath   string
    Type      Type      // 字段的类型
    Tag       StructTag // 字段的标签
    Offset    uintptr   // 字段在结构体中的字节偏移量
    Index     []int     // 用于Type.FieldByIndex时的索引切片
    Anonymous bool      // 是否匿名字段
}

5.2 与结构体相关的方法

任意值通过reflect.TypeOf()获得反射对象信息后,如果它的类型是结构体,可以通过反射值对象(reflect.Type)的NumField()Field()方法获得结构体成员的详细信息。

reflect.Type中与获取结构体成员相关的的方法如下表所示。

方法名以及返回类型 说明
Field(i int) StructField 根据索引,返回索引对应的结构体字段的信息。
NumField() int 返回结构体成员字段数量。
FieldByName(name string) (StructField, bool) 根据给定字符串返回字符串对应的结构体字段的信息。
FieldByIndex(index []int) StructField 多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的信息。
FieldByNameFunc(match func(string) bool) (StructField,bool) 根据传入的匹配函数匹配需要的字段。
NumMethod() int 返回该类型的方法集中方法的数目
Method(int) Method 返回该类型方法集中的第i个方法
MethodByName(string) Method 根据方法名返回该类型方法集中的方法
// 例子
package main

import (
    "fmt"
    "reflect"
)

type student struct {
    Name   string `json:"name"`
    Gender string `json:"gender"`
    Age    int    `json:"age"`
}

// 给student添加两个方法 Study和Sleep(注意首字母大写)
func (s student) Study() string {
    msg := "好好学习,天天向上。"
    fmt.Println(msg)
    return msg
}

func (s student) Sleep() string {
    msg := "好好睡觉,快快长大。"
    fmt.Println(msg)
    return msg
}

func main() {
    // 定义一个匿名结构体并初始化
    stu1 := student{
        Name:   "A7cc",
        Gender: "男",
        Age:    18,
    }
    // 属性-----------------------
    t := reflect.TypeOf(stu1)
    // 使用NumField方法返回结构体成员字段数量
    // 通过for循环遍历结构体的所有字段信息
    for i := 0; i < t.NumField(); i++ {
        // 使用Field方法根据索引,返回索引对应的结构体字段的信息
        field := t.Field(i)
        fmt.Printf("结构体字段名字:%s 结构体字段索引:%d 结构体字段类型:%v 结构体字段的json tag:%v\n", field.Name, field.Index, field.Type, field.Tag.Get("json"))

    }

    // 使用FieldByName方法通过字段名获取指定结构体字段信息
    if ageField, ok := t.FieldByName("Age"); ok {
        fmt.Printf("结构体字段名字:%s 结构体字段索引:%d 结构体字段类型:%v 结构体字段的json tag:%v\n", ageField.Name, ageField.Index, ageField.Type, ageField.Tag.Get("json"))
    }

    // 方法------------------------
    v := reflect.ValueOf(stu1)

    // 使用NumMethod方法返回该类型的方法集中方法的数目
    // 通过for循环遍历结构体的所有方法信息
    for i := 0; i < v.NumMethod(); i++ {
        // 使用Method方法可以返回该类型方法集中的第i个方法
        fmt.Printf("结构体方法名字:%s\n", t.Method(i).Name)
        fmt.Printf("结构体方法类型以及返回值类型:%s\n", v.Method(i).Type())
        // 通过反射调用方法传递的参数必须是 []reflect.Value 类型
        var args = []reflect.Value{}
        // Call调用结构体方法
        v.Method(i).Call(args)
    }

    // 使用MethodByName方法根据方法名返回该类型方法集中的方法
    fmt.Println(v.MethodByName("Sleep").Type())
}

陆 使用反射的建议

反射是一个强大并富有表现力的工具,能让我们写出更灵活的代码。但是反射不应该被滥用,原因有以下三个。

  • a.基于反射的代码是极其脆弱的,反射中的类型错误会在真正运行的时候才会引发panic,那很可能是在代码写完的很长时间之后。
  • b.大量使用反射的代码通常难以理解。
  • c.反射的性能低下,基于反射实现的代码通常比正常代码运行速度慢一到两个数量级。
avatar
小C&天天

修学储能 先博后渊


今日诗句