38、Go语言基础之反射

作者: 温新

分类: 【Go基础】

阅读: 277

时间: 2023-12-05 00:32:20

hi,我是温新

什么是反射

反射(Reflection)是一种编程技术,它允许程序在运行时检查和操作变量、方法、结构等程序结构的能力。

Go语言中也提供了反射机制,通过reflect包可以实现这一功能。

反射的基本概念:

反射是一种在运行时检查和操作程序结构的技术。它允许你在程序运行时动态地查看和修改变量、类型和函数等信息。

reflect包:

在Go语言的反射机制中,任何接口值都由是一个具体类型具体类型的值两部分组成的。Go reflect包提供了处理反射的工具。你可以使用这个包来获取变量的类型、值,检查方法和字段等。

反射的基本操作

TypeOf

在 Go 语言中,使用 reflect.TypeOf 获取任意值的类型对象。

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var name string = "王美丽"
	var age int8 = 19

	reflectType(name) // 类型为:string
	reflectType(age) // 类型为:int8
}

// 定义一个函数,用于获取变量的类型
func reflectType(data interface{}) {
	v := reflect.TypeOf(data)
	fmt.Printf("类型为:%v\n", v)
}

type name 和 type kind

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

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var x *float32 // 指针
	var y myInt    // 自定义类型
	var z rune     // 类型别名

	getReflectType(x)
	getReflectType(y)
	getReflectType(z)

	var lucy = user{"露西"}
	getReflectType(lucy)
}

type myInt int

type user struct {
	name string
}

func getReflectType(data interface{}) {
	v := reflect.TypeOf(data)
	fmt.Printf("类型为:%v,种类为:%v\n", v.Name(), v.Kind())
}

输出结果:

类型为:,种类为:ptr
类型为:myInt,种类为:int
类型为:int32,种类为:int32
类型为:user,种类为:struct

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        // 底层指针
)

ValueOf

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

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

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

通过反射获取值

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var x float32 = 3.14
	var y int64 = 100
	GetReflectValue(x) // 类型为 float32,值为 3.140000
	GetReflectValue(y) // 类型为 Int64,值为 100

	z := reflect.ValueOf(1)
	fmt.Printf("类型 z 为:%T\n", z) // 类型 z 为:reflect.Value
}

func GetReflectValue(data interface{}) {
	// 通过反射获取值
	v := reflect.ValueOf(data)
	// 获取值的类型
	k := v.Kind()

	switch k {
	case reflect.Int64:
		// v.Int() 从反射中获取整型的原始值,再强制转类型
		fmt.Printf("类型为 Int64,值为 %d\n", int64(v.Int()))
	case reflect.Float32:
		fmt.Printf("类型为 float32,值为 %f\n", float32(v.Float()))
	case reflect.Float64:
		fmt.Printf("类型为 float64,值为 %f\n", float64(v.Float()))
	}
}

通过反射设置值

要在函数中通过反射修改变量的值,需要注意函数参数传递的是值拷贝,必须传递变量地址才能修改变量的值。但是在反射中使用Elem()方法来获取指针对应的值。案例如下:

package main

import (
	"fmt"
	"reflect"
)

func main() {
	name := "王美丽"
	reflectSetValue2(&name)
	fmt.Println(name) // 王大丽

	reflectSetValue(name) // 报错
}

func reflectSetValue(data interface{}) {
	v := reflect.ValueOf(data)
	if v.Kind() == reflect.String {
		// 不能修改值,因为函数调用是值的拷贝,只有传递指针时才能修改对应的值
		v.SetString("王大丽1")
	}
}

func reflectSetValue2(data interface{}) {
	v := reflect.ValueOf(data)
	// 反射中通过 eLem 方法获取指针对应的值
	if v.Elem().Kind() == reflect.String {
		v.Elem().SetString("王大丽")
	}
}

isNil() 和 isValid()

isNil()

func (v value) IsNil() bool

IsNil() 方法用于检查反射值是否为 nil,通常用于指针类型。如果反射值代表一个 nil 指针,则 IsNil() 返回 true,否则返回 false

对于非指针类型的反射值,调用 IsNil() 方法会引发 panic。

isValid()

func (v value) IsValid() bool

IsValid() 方法用于检查反射值是否有效,即是否引用了一个合法的 Go 值。如果反射值有效,它返回 true,否则返回 false。v 除了IsValid、String、Kind之外的方法都会导致 panic。

一个反射值在以下情况下被认为是无效的:

​ 1、它是一个零值。

​ 2、它是一个未初始化的反射值。

​ 3、它表示一个不允许被读取或修改的值,例如不可导出的字段。

案例如下:

package main

import (
	"fmt"
	"reflect"
)

func main() {
	// 定义一个空指针
	var x *int
	fmt.Println("变量 x 的值是 IsNil:", reflect.ValueOf(x).IsNil()) // true

	// nil 值
	fmt.Println("nil IsValid:", reflect.ValueOf(nil).IsValid()) // false

	// 实例化一个匿名结构体
	y := struct {
	}{}
	// 在结构体查找 id 字段
	fmt.Println("不存在的结构体成员:", reflect.ValueOf(y).FieldByName("id").IsValid()) // false
	// 在结构体中查找方法
	fmt.Println("不存在的结构体方法:", reflect.ValueOf(y).MethodByName("getName").IsValid()) // false

	// map
	z := map[string]int{}
	// map 中查找一个不存在的 key
	fmt.Println("map 中不存在的 key:", reflect.ValueOf(z).MapIndex(reflect.ValueOf("美丽")).IsValid()) // false
}

输出结果:

变量 x 的值是 IsNil: true
nil IsValid: false
不存在的结构体成员: false
不存在的结构体方法: false
map 中不存在的 key: false

结构体反射

通过 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, bool) 根据方法名返回该类方法集中的方法

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      // 是否匿名字段
}

结构体反射案例如下:

package main

import (
	"fmt"
	"reflect"
)

func main() {
	stu1 := Student{"王美丽", 100}
	t := reflect.TypeOf(stu1)
	fmt.Println(t.Name(), t.Kind())

	// 遍历结构体所有字段信息
	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		fmt.Printf("名字:%s 索引:%d,类型:%v json 标签:%v\n", field.Name, field.Index, field.Type, field.Tag)
	}

	// 通过字段名获取指定结构体字段信息
	if scoreField, ok := t.FieldByName("score"); ok {
		fmt.Printf("名字:%s 索引:%d 类型:%v json 标签:%v\n", scoreField.Name, scoreField.Index, scoreField.Type, scoreField.Tag.Get("json"))
	}

	printMethod(stu1)
}

type Student struct {
	name  string `json:"name"`
	score int    `json:"score"`
}


func (s Student) Study() string {
	msg := "好好学习,天天向上"
	fmt.Println(msg)
	return msg
}

func (s Student) Sleep() string {
	msg := "太阳当空照"
	fmt.Println(msg)
	return msg
}

// 通过反射获取方法
func printMethod(data interface{}) {
	t := reflect.TypeOf(data)
	v := reflect.ValueOf(data)
	fmt.Println(t.NumMethod())

	for i := 0; i < v.NumField(); i++ {
		methodType := v.Method(i).Type()
		fmt.Printf("方法名:%s\n", t.Method(i).Name)
		fmt.Printf("方法:%s\n", methodType)
        
        // 通过反射调用方法传递的参数必须是 []reflect.Value 类型
		var args = []reflect.Value{}
		v.Method(i).Call(args)
	}
}

输出结果:

Student struct
名字:name 索引:[0],类型:string json 标签:json:"name"
名字:score 索引:[1],类型:int json 标签:json:"score"
名字:score 索引:[1] 类型:int json 标签:score
2
方法名:Sleep
方法:func() string
太阳当空照
方法名:Study
方法:func() string
好好学习,天天向上

反射是把双刃剑

反射在 Go 语言中是一项强大的功能,但也是需要谨慎使用的。以下是一些原因解释为什么反射需要小心使用:

  • 1、基于反射的代码比较脆弱,其反射中的类型错误是在代码真正运行时才触发 panic
  • 2、大量使用反射,代码将变得难以理解;
  • 3、反射会增加性能开销,因为它在运行时进行类型检查。
请登录后再评论