6、反射

反射

反射,就是建立在类型之上的,Golang 的指定类型的变量的类型是静态的(也就是指定int、string这些的变量,它的 type 是 static type),在创建变量的时候就已经确定。

反射主要与 Golang 的interface类型相关(它的 type 是 concrete type),只有 interface 类型才有反射一说。

Go 程序的反射系统无法获取到一个可执行文件空间中或者是一个包中的所有类型信息,需要配合使用标准库中对应的词法、语法解析器和抽象语法树(AST)对源码进行扫描后获得这些信息。

Go 语言提供了一种机制在运行时更新变量和检查它们的值、调用它们的方法和它们支持的内在操作,但是在编译时并不知道这些变量的具体类型。这种机制被称为反射。

golang中的反射

Go 语言中的反射与其他语言有比较大的不同,Golang 中的发射主要涉及到两个基本概念reflect.Typereflect.Value,它们也是 Go 语言包中 reflect 包里最重要的两个类型。

在 Golang 中对所有接口进行反射,都可以得到一个包含 Type 和 Value 的信息结构。顾名思义,Type 主要表达的是被反射的这个变量本身的类型信息,而 Value 则为该变量实例本身的信息。

反射三大定律

  1. Reflection goes from interface value to reflection object.

  2. Reflection goes from reflection object to interface value.

  3. To modify a reflection object, the value must be settable.

  4. 反射可以将接口类型变量转换为“反射类型对象”;

  5. 反射可以将 “反射类型对象”转换为接口类型变量;

  6. 如果要修改 “反射类型对象” 其类型必须是可写的;

reflect包

Go语言中的反射是由 reflect 包提供支持的,它定义了两个重要的类型 reflect.Type 和 reflect.Value 任意接口值在反射中都可以理解为由 reflect.Type (接口)和 reflect.Value (结构体)组成,并且 reflect 包提供了 reflect.TypeOf 和 reflect.ValueOf 两个函数来获取任意对象的 reflect.Value 和 reflect.Type。

reflect.Type

反射获取Type

func TypeOf(i any) Type

a = 10
reflect.TypeOf(a) //int

Name()获取类型

func (t *rtype) Name() string

a = 10
reflect.TypeOf(a).Name() //int

reflect.Value

反射获取Value

func ValueOf(i any) Value

a := 10
reflect.ValueOf(a) //10

Kind()

Kind() 方法返回的变量的数据类型种类,我们可以用reflect包定义的常量来进行类型判断。

func (v Type) Kind() Kind
func (v Value) Kind() Kind

a = 10
reflect.TypeOf(a).Kind() == reflect.Int //true
reflect.ValueOf(a).Kind() == reflect.Int //true

类型和种类的区别

Go语言程序中的类型(Type)指的是系统原生数据类型,如 int、string、bool、float32 等类型,以及使用 type 关键字定义的类型,这些类型的名称就是其类型本身的名称,使用Type的Name()方法获取类型字符串。

但在反射中,当需要区分一个大品种的类型时,就会用到种类(Kind),种类在reflect包中使用枚举进行了定义:

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
	Pointer  //const Ptr = Pointer
	Slice
	String
	Struct
	UnsafePointer
)

Map、Slice、Chan 属于引用类型,使用起来类似于指针,但是在种类常量定义中仍然属于独立的种类,不属于 Ptr。type A struct{} 定义的结构体A属于 Struct 种类,*A 属于 Ptr。

Elem()

Go语言程序中通过指针获取反射对象reflect.Type、reflect.Value时,可以通过Elem() 方法获取这个指针指向实际值的reflect.Type、reflect.Value,这个获取过程被称为取元素,等效于对指针类型变量做了一个*操作

例如,对一个*int的指针变量,它的Type对应的类型Name是"",种类Kind是ptr,而Value获取的值就是指针保存的值,即是一个地址,所以,如果我们想要使用指针指向值的reflect.Name、reflect.Type,就需要使用Elem()方法进行解引用

i := 100
iPtr := &i
typ := reflect.TypeOf(iPtr)
val := reflect.ValueOf(iPtr)
fmt.Printf("type name is %s,type kind is %s\n", typ.Name(), typ.Kind()) //type name is ,type kind is ptr
fmt.Printf("value is %v\n", val)                                        //value is 0xc0000140a8

typEl := typ.Elem()
valEl := val.Elem()
fmt.Printf("type name is %s,type kind is %s\n", typEl.Name(), typEl.Kind()) //type name is int,type kind is int
fmt.Printf("value is %v\n", valEl)                                          //value is 100

反射操作变量

通过Value获取值

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

通过Value修改值

CanSet()

func (v Value) CanSet() bool

这个方法是用来检测Value是否为可设置的值,因为golang 里面的所有函数调用(参数为变量和地址)都是值复制,所以这里在调用 reflect.ValueOf 的时候,已经复制了一个 x 传递进去了,这里获取到的 v 是一个 x 复制体的 value,所以此时,我们赋值的value进行设置,是没有任何意义的,例如:

type Stu struct{}
a := 10
s := new(Stu)
fmt.Println(reflect.ValueOf(a).CanSet())  //false
fmt.Println(reflect.ValueOf(&a).CanSet()) //false
fmt.Println(reflect.ValueOf(s).CanSet())  //false

无论传入ValueOf()函数的是指针(地址)还是变量,CanSet()函数返回的都是false,因为reflect.Value中保存的都是值或地址的副本,而不是变量as指向的真实值,如果要可以设置,就需要将真实值的指针传入ValueOf(),并通过Elem()方法,获取封装了指针指向的实际值的reflect.Value

type Stu struct{}
a := 10
s := new(Stu)
fmt.Println(reflect.ValueOf(&a).Elem().CanSet()) //true
fmt.Println(reflect.ValueOf(s).Elem().CanSet())  //true

Set()

其中,Set 方法接受的参数是一个reflect.Value类型

func (v Value) Set(x Value)
a := 10
b := 100
bVal := reflect.ValueOf(b)
reflect.ValueOf(&a).Elem().Set(bVal)
fmt.Println(a) //100
方法 描述
func (v Value) Setlnt(x int64) 使用 int64 设置值。当值的类型不是 int、int8、int16、 int32、int64 时会发生宕机
func (v Value) SetUint(x uint64) 使用 uint64 设置值。当值的类型不是 uint、uint8、uint16、uint32、uint64 时会发生宕机
func (v Value) SetFloat(x float64) 使用 float64 设置值。当值的类型不是 float32、float64 时会发生宕机
func (v Value) SetBool(x bool) 使用 bool 设置值。当值的类型不是 bod 时会发生宕机
func (v Value) SetBytes(x []byte) 设置字节数组 []bytes值。当值的类型不是 []byte 时会发生宕机
func (v Value) SetString(x string) 设置字符串值。当值的类型不是 string 时会发生宕机

以上方法,在 reflect.Value 的 CanSet 返回 false 仍然修改值时会发生宕机。

package main

import (
	"errors"
	"fmt"
	"reflect"
)

func setYear(num any) error {
	val := reflect.ValueOf(num)
	//修改值的前提就是传入的是ptr
	if val.Kind() != reflect.Pointer {
		return errors.New("Must be a Ptr")
	}
	//取出指针指向的实际值
	val = val.Elem()
	if val.Kind() == reflect.Int {
		val.SetInt(2020)
	} else if val.Kind() == reflect.String {
		val.SetString("2020")
	} else {
		return errors.New("Type error")
	}
	return nil
}

func main() {
	var i int
	err := setYear(&i)
	fmt.Println(err, i) //<nil> 2020
	var s string
	err = setYear(&s)
	fmt.Println(err, s) //<nil> 2020
}

反射操作切片

替换全部切片

package main

import (
	"fmt"
	"reflect"
)

func myFunc(s any) {
	val := reflect.ValueOf(s).Elem()
	v := reflect.ValueOf([]int{10, 20, 30})
	val.Set(v)
}
func main() {
	slice := []int{1, 2, 3}
	myFunc(&slice)
	fmt.Println(slice) //[10 20 30]
}

根据索引修改

注意:此时传入的应该是变量,而不是指针(地址)

package main

import (
	"fmt"
	"reflect"
)

func myFunc(s any) {
	val0 := reflect.ValueOf(s).Index(0)
	val0.SetInt(100)
}
func main() {
	slice := []int{1, 2, 3}
	myFunc(slice)
	fmt.Println(slice) //[10 20 30]
}

反射操作结构体

Type

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

StructField

reflect.Type 的 Field() 方法返回 StructField 结构,这个结构描述结构体的成员信息,通过这个信息可以获取成员与结构体的关系,如偏移、索引、是否为匿名字段、结构体标签(Struct Tag)等,而且还可以通过 StructField 的 Type 字段进一步获取结构体成员的类型信息。StructField 的结构如下:

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

常用方法

方法 说明
func (t *structType) Field(i int) (f StructField) 根据索引,返回索引对应的结构体字段的信息。当t不是结构体或索引超界时会引发 pannic
func (t *rtype) NumField() int 返回结构体成员字段数量。当类型不是结构体或索引超界时引发 pannic
func (t *structType) FieldByName(name string) (f StructField, present bool) 根据给定字符串返回字符串对应的结构体字段的信息。没有找到时 bool 返回 false,当类型不是结构体或索引超界时引发 pannic
func (t *structType) FieldByIndex(index []int) (f StructField) 多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的信息。没有找到时返回零值。当类型不是结构体或索引超界时引发 pannic
func (t *structType) FieldByNameFunc(match func(string) bool) (result StructField, ok bool) 根据匹配函数匹配需要的字段。当值不是结构体或索引超界时引发 pannic
type Stu struct {
    name string
    age  int
}

s := Stu{name: "lucy", age: 18}
typ := reflect.TypeOf(s)
fmt.Println(typ.Name(), typ.Kind())     //Stu struct
fmt.Println(typ.Field(0))               //{name main string  0 [0] false}
fmt.Println(typ.NumField())             //2
fmt.Println(typ.FieldByName("name"))    //{name main string  0 [0] false} true
fmt.Println(typ.FieldByIndex([]int{0})) //{name main string  0 [0] false}
typ.FieldByNameFunc(func(s string) bool {
    fmt.Println(s)
    return false
}) // name age

StructTag

通过 reflect.Type 获取结构体成员信息 reflect.StructField 结构中的 Tag 被称为结构体标签(StructTag)。结构体标签是对结构体字段的额外信息标签。

JSON、BSON 等格式进行序列化及对象关系映射(Object Relational Mapping,简称 ORM)系统都会用到结构体标签,这些系统使用标签设定字段在处理时应该具备的特殊属性和可能发生的行为。这些信息都是静态的,无须实例化结构体,可以通过反射获取到。

StructTag具有两个方法

func (tag StructTag) Get(key string) string //根据 Tag 中的键获取对应的值
func (tag StructTag) Lookup(key string) (value string, ok bool) //根据 Tag 中的键,查询值是否存在
type Stu struct {
    name string `json:"name" yaml:"n"`
    age  int    `json:"age" yaml:"a"`
   //表示json序列化的时候该字段忽略
}

s := Stu{name: "lucy", age: 18}
typ := reflect.TypeOf(s)

tag1 := typ.Field(0).Tag
tag2 := typ.Field(1).Tag
//name字段Tag为,json:name,yaml:n
fmt.Printf("name字段Tag为,json:%s,yaml:%s\n", tag1.Get("json"), tag1.Get("yaml"))
//age字段Tag为,json:age,yaml:a
fmt.Printf("age字段Tag为,json:%s,yaml:%s\n", tag2.Get("json"), tag2.Get("yaml"))

Value

常用方法

方 法 备 注
func (v Value) Field(i int) Value 根据索引,返回索引对应的结构体成员字段的反射值对象。当值不是结构体或索引超界时发生宕机
func (v Value) NumField() int 返回结构体成员字段数量。当值不是结构体或索引超界时发生宕机
func (v Value) FieldByName(name string) Value 根据给定字符串返回字符串对应的结构体字段。没有找到时返回零值,即IsValid()返回false,当值不是结构体或索引超界时发生宕机
func (v Value) FieldByIndex(index []int) Value 多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的值。 没有找到时返回零值,即IsValid()返回false,当值不是结构体或索引超界时发生宕机
func (v Value) FieldByNameFunc(match func(string) bool) Value 根据匹配函数匹配需要的字段。找到时返回零值,当值不是结构体或索引超界时发生宕机
type Stu struct {
    name string
    age  int
}

s := Stu{name: "lucy", age: 18}
val := reflect.ValueOf(s)
fmt.Println(val.Field(0))               //lucy
fmt.Println(val.NumField())             //2
fmt.Println(val.FieldByName("name"))    //lucy
fmt.Println(val.FieldByIndex([]int{0})) //lucy
val.FieldByNameFunc(func(s string) bool {
    fmt.Println(s)
    return false
}) //name age

修改结构体成员的值

注意:反射修改变量的值,要求必须可寻址,也就是必须传入的是指针;对于修改结构体成员,除了要求可寻址以外,还要要求必须可导出(也就是成员必须是public的,首字母大写)

package main

import (
	"errors"
	"fmt"
	"reflect"
)

type Stu struct {
	Name string
	age  int
}

func myFunc(s any) error {
	val := reflect.ValueOf(s)
	if val.Kind() != reflect.Ptr {
		return errors.New("Mast be a ptr")
	}
	val = val.Elem()
	nameVal := val.FieldByName("Name")
	nameVal.SetString("tom")
	return nil
}
func main() {
	s := Stu{Name: "lucy", age: 18}
	err := myFunc(&s)
	fmt.Println(s, err) //{tom 18} <nil>
}

调用结构体方法

注意:如果调用ValueOf()传入的是结构体变量,那么只能调用接收器为结构体变量的方法;如果调用ValueOf()传入的是结构体指针,那么既可以调用接收器是结构体指针的方法,也可以调用接收器是结构体变量的方法

func (v Value) MethodByName(name string) Value
//调用上面方法返回的Value实例,参数为方法参数切片,返回值为方法返回值切片
func (v Value) Call(in []Value) []Value 
package main

import (
	"fmt"
	"reflect"
)

type Stu struct {
	name string
	age  int
}

func (s *Stu) Info() string {
	return fmt.Sprintf("name:%s,age:%d", s.name, s.age)
}

func main() {
	s := Stu{name: "lucy", age: 18}
	m := reflect.ValueOf(&s).MethodByName("Info")
	a := m.Call(nil)
	fmt.Println(a[0]) //name:lucy,age:18
}