Golang 反射

前言

反射(reflection)是一种能够检查程序在运行时的变量类型和值的机制。Go的反射机制定义在reflect包中。使用反射,可以动态地调用对象的方法或访问其字段,即使在编写代码时并不知道这些方法或字段的具体存在

反射主要涉及到reflect.Typereflect.Value这两个类型。reflect.Type代表Go值的类型,而reflect.Value代表Go值的具体值

在使用反射时需要注意的是,反射操作通常比直接操作要慢,因此在性能敏感的代码中应当谨慎使用反射。另外,过度使用反射可能会使代码难以理解和维护

获取值

基本步骤是通过reflect.ValueOf()函数获取反射值对象,然后使用该对象的方法来获取具体的值

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var (
		x float64 = 11.2
		y int     = 55
		z string  = "hello"
	)

	// 获取x的反射值对象
	valueOfX := reflect.ValueOf(x)
	// 获取实际的float64值
	fmt.Println("value of x:", valueOfX.Float())

	// 获取y的反射值对象
	valueOfY := reflect.ValueOf(y)
	// 获取实际的int值
	fmt.Println("value of y:", valueOfY.Int())

	// 获取z的反射值对象
	valueOfZ := reflect.ValueOf(z)
	// 获取实际的string值
	fmt.Println("value of z:", valueOfZ.String())
}

创建了三个变量x、y和z,分别是float64、int和string类型。对于每个变量,用reflect.ValueOf()获取其反射值对象,并根据变量的基本类型,使用相应的方法来获取实际的值。例如,对于float64类型的变量,我们调用Float()方法来获取它的值。类似地,对于int和string,我们分别调用Int()和String()方法

反射值对象reflect.Value提供了一系列的方法来获取具体类型的值,包括但不限于Bool()Bytes()Complex()Float()Int()String()等。每个方法对应于原始值的类型,尝试获取与值类型不匹配的类型会导致运行时panic

修改值

使用reflect包可以修改反射对象的值,须满足以下条件:

  • 反射对象reflect.Value必须是可寻址的(可设置的)
  • 反射对象对应的值必须是可导出的(即字段名首字母大写)

要修改一个变量的值,需要通过reflect.ValueOf获得反射值对象,并通过.Elem()方法获取指针指向的值的反射值对象

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var x = 2.7
	fmt.Println("before:", x)

	// 获取变量x的反射值对象
	p := reflect.ValueOf(&x) // 注意:这里传入的是x的地址
	// 获取反射值对象对应的值(需要是可设置的)
	v := p.Elem()

	// 判断反射值对象是否可以改变
	if v.CanSet() {
		// 修改反射值对象的值
		v.SetFloat(11.3)
	}

	fmt.Println("after:", x)
}

首先获取x的地址的反射对象p,然后通过调用p.Elem()获取x值的反射对象v。由于v是通过指针获取的,所以它是可寻址的,然后通过v.SetFloat来修改x的值

reflect.Type()reflect.Kind() 的使用

reflect.Type 表示一个Go值的具体类型,包括其名称和包路径。这个类型是接口类型,提供了关于Go值的详细类型信息,比如结构体的字段信息、一个函数的签名等

reflect.Kind 则表示一个Go值的基本分类,或者说是其底层类型。在Go语言中,基本分类包括如intfloat64boolstructslice 等。每一个 reflect.Type 都对应一个 reflect.Kind,但是不同的 reflect.Type 可能对应相同的 reflect.Kind。例如,所有结构体类型的 reflect.Kind 都是 reflect.Struct

举例说明

type MyStruct struct {
	Field1 int
	Field2 string
}

s := MyStruct{Field1: 10, Field2: "Hello"}
v := reflect.ValueOf(s)

fmt.Println("type:", v.Type()) // 输出s的具体类型,例如 "main.MyStruct"
fmt.Println("kind:", v.Kind()) // 输出s的基本分类,例如 "struct"

结构体中的反射

  1. 获取值
    获取到反射值对象后,可以是通过.Field()拿到元数据,然后点其中的方法,拿到字段属性
package main

import (
	"fmt"
	"reflect"
)

type User struct {
	ID     int    `json:"id"`
	Name   string `json:"name"`
	Age    int    `json:"age,omitempty"`
	weight float64
}

func GetUser(user User) {
	t := reflect.TypeOf(user)           // 获取user变量的类型对象
	v := reflect.ValueOf(user)          // 获取user变量的值对象
	for i := 0; i < v.NumField(); i++ { // 以结构体字段个数,遍历
		f := t.Field(i) // 获取第i个字段的元数据, 以第二个字段为例:{Name  string json:"name" 8 [1] false}
		fmt.Println(f.Name, f.Type, f.Tag, f.Offset, f.Index, f.Anonymous, f.PkgPath)  // 下面解释提到
		fmt.Printf("%#v\n", f.Tag.Get("json")) // 打印字段的json标签值
		fmt.Println(v.Field(i))                // 获取第i个字段的值
		fmt.Println("----------------------------")
	}

}

func main() {
	user := User{
		ID:     1,
		Name:   "张三",
		Age:    20,
		weight: 45.5,
	}
	GetUser(user)
}

运行后,控制台输出:

ID int json:"id" 0 [0] false
"id"
1
----------------------------
Name string json:"name" 8 [1] false
"name"
张三
----------------------------
Age int json:"age,omitempty" 24 [2] false
"age,omitempty"
20
----------------------------
weight float64  32 [3] false main
""
45.5
----------------------------

字段元数据中的其他字段属性解释:

  • f.Name: 字段名称,例如 "Name"
  • f.Type: 字段的类型,例如 reflect.TypeOf("") 表示 string 类型
  • f.Tag: 字段的标签(Tag),是结构体中字段后定义的额外信息,例如 json:"name"
  • f.Offset: 字段在结构体内存中的偏移量,表示该字段在结构体中的起始字节位置。
  • f.Index: 字段的索引切片,表示嵌套字段的访问索引。对于顶层字段,这将是单个元素的切片,例如 [1]
  • f.Anonymous: 表示字段是否是匿名字段(即嵌入字段),布尔值 truefalse
  • f.PkgPath: 如果字段是非导出的(即小写开头),则为字段的包路径。如果字段是导出的(即大写开头),它将是空字符串。我的例子中,就是main
  1. 再来看看修改值
    首先&传入结构体指针,然后.Elem()获取指针指向的值的反射值对象,再然后就同取值一样,.Field()之类的,最后修改就是.SetInt.SetFloatSetString 等这些,同普通类型的获取值基本一致
package main

func SetUser(user *User) { // 或者用 any 来替代*User,但应处理异常
	v := reflect.ValueOf(user).Elem()
	v.FieldByName("Age").SetInt(22)
}
func main() {
	user := User{
		ID:     1,
		Name:   "张三",
		Age:    20,
		weight: 45.5,
	}
	SetUser(&user)
	fmt.Println(user.Age)  // 22

}

这里需要注意:设置的字段需要是可导出的(大写)且可寻址的,否则会引发 panic

  1. 反射调用结构体的方法
    经过前面学习,我们知道字段是通过.Field()之类的方法来找到的,同样地,方法是通过.Method()之类的方法来找到
    调用结构体方法,是通过.Call(in []Value) []Value来实现
package main

import (
	"fmt"
	"reflect"
)

type MyStruct struct {
	Value int
}

// Add 是一个指针接收器方法
func (s *MyStruct) Add(amount int) {
	s.Value += amount
}

func main() {
	// 创建结构体实例
	instance := MyStruct{Value: 100}

	// 获取reflect.Value类型的实例
	val := reflect.ValueOf(&instance)

	// 寻找方法
	method := val.MethodByName("Add")

	// 检查方法是否存在
	if !method.IsValid() {
		fmt.Println("Method not found.")
		return
	}

	// 参数必须作为reflect.Value的切片传入
	args := []reflect.Value{reflect.ValueOf(20)}

	// 调用方法
	method.Call(args)

	// 输出结果
	fmt.Println(instance.Value)  // 120
}

你可能感兴趣的:(golang)