【Go】反射

🌈 个人主页:Zfox_

🔥 系列专栏:Go

目录

  • [一:🔥 什么是反射](#一:🔥 什么是反射)
  • [二:🔥 Go 语言反射](#二:🔥 Go 语言反射)
    • [🦋 reflect.TypeOf()](#🦋 reflect.TypeOf())
    • [🦋 reflect.ValueOf()](#🦋 reflect.ValueOf())
  • 三:🔥反射使用
  • [四:🔥 反射的优缺点](#四:🔥 反射的优缺点)
  • [五:🔥 共勉](#五:🔥 共勉)

一:🔥 什么是反射

反射可以认为是程序在运行时的一种能力,反射可以在程序运行时访问、检测和修改它本身状态,比如在程序运行时可以检查变量的类型和值,调用它们的方法,甚至修改它们的值。使用反射可以增加程序的灵活性,简单来说,反射就是程序在运行时能够检测自身和修改自身的一种能力。

二:🔥 Go 语言反射

对于很多的高级语言都实现了反射,像 Java,Python。在 Go 语言中,反射在 Go 语言内置的 reflect 包下实现。Go 语言中的反射建立在 Go 的类型系统之上,并且与接口密切相关。通过前面的学习我们知道 Go 语言的空接口包含类型 (Type) 和值 (Value) 两个部分,在反射里,也要用到类型 (Type) 和值 (Value)。

reflect 包中定义了 reflect.Type 和 reflect.Value,正好对应我们前面所说的Type 和 Value。要注意的是 reflect.Type 是一个接口而 reflect.Value 是一个具体的结构体。在 reflect.Type 接口中定义了很多跟类型相关的方法,而reflect.Value 则是绑定了很多跟值相关的方法。

🦋 reflect.TypeOf()

由于 reflect.Type 是一个接口 ,所以只有当某个类型实现了这个接口,我们才能获取到它的类型,同时,在 reflect 包内,类型描述符是未导出类型,所以我们只能通过 reflect.TypeOf() 方法获取 reflect.Type 类型的值。

我们首先看一个例子,看下 reflect.TypeOf() 的常用用法:

go 复制代码
package main

import (
    "fmt"
    "reflect"
)

type Student struct {
    Name string
    Age  int
}

func main() {
    var num int64 = 100
    t1 := reflect.TypeOf(num)
    fmt.Println(t1.String())

    st := Student{
       Name: "zhangsan",
       Age:  18,
    }
    t2 := reflect.TypeOf(st)
    fmt.Println(t2.String())
}

运行结果:

go 复制代码
int64
main.Student

可以看到对于基础类型和 struct 类型通过调用 reflect.TypeOf() 都打印出了对应的类型信息。注意 reflect.TypeOf 返回的是一个 reflect.Type 接口类型,我们通过调用这个接口的 String() 方法,得到最终的字符串信息。

在前面学习 interface 的章节中,我们知道一个具体的数据类型是可以赋值给一个 interface 类型的,反过来则不行,要用到 interface 的断言。在一个interface 赋值之后,其实是对应了两个类型,一个是静态类型,就是在程序编译期就确定的类型,interface 的静态类型就是接口 interface,同时当 interface 赋值之后,他还有一个动态类型,就是被赋值的那个数据的具体类型,假设在上例中,我们将 st 赋值给一个空 interface,那么这个 interface 的动态类型就是 Student。

对一个数据对象进行反射操作,其实是首先将具体对象类型转化为一个 interface 类型,然后再将 interface 类型转化为 reflect 包下的反射类型,反射类型里的类型信息和值信息其实就是对应着这个中间类型 interface 的类型和值。

reflect.TypeOf() 方法获取的就是这个 interface{} 中的类型部分。

🦋 reflect.ValueOf()

同理,reflect.ValueOf() 方法自然就是获取接口中的值部分,reflect.ValueOf() 的返回值其实就是一个 reflect.Value 结构体。

go 复制代码
import (
    "fmt"
    "reflect"
)

type Student struct {
    Name string
    Age  int
}

func main() {
    var num int64 = 100
    v1 := reflect.ValueOf(num)
    fmt.Println(v1)
    fmt.Println(v1.String())

    st := Student{
       Name: "zhangsan",
       Age:  18,
    }
    v2 := reflect.ValueOf(st)
    fmt.Println(v2)
    fmt.Println(v2.String())
}

运行结果:

go 复制代码
100
<int64 Value>
{zhangsan 18}
<main.Student Value>

注意到这里 fmt.Println(v1) 和 fmt.Println(v1.String()) 打印的不一样,上面说了 reflect.ValueOf() 的返回值就是一个 reflect.Value 结构,但是 fmt.Println(v1) 却打印出了具体的值,这是因为 fmt.Println 的参数是一个接口类型,在执行过程中有一些类型转换,对 reflect.Value 结构做了特殊处理。

三:🔥反射使用

🦋 值对象

reflect 包下跟值对象相关的常用函数或方法:

函数/方法 说明
reflect.TypeOf() 获取某个对象的反射类型实现 (reflect.Type)
reflect.ValueOf() 获取某个对象的反射值对象(reflect.Value)
reflect.Value.NumField() 获取结构体的反射值对象中的字段个数,只对结构体类型有效
reflect.Value.Field(i) 获取结构体的反射值对象中的第i个字段,只对结构体类型有效
reflect.Kind() 从反射值对象中获取该值的种类
reflect.Value.MapKeys() 对 map 的每个键的 reflect.Value 对象组成的一个切片
reflect.Value.MapIndex(i) 根据map的某个键的reflect.Value对象,返回值的reflect.Value对象
reflect.Value.Len() 对切片或数组的反射对象求切片或数组的长度
reflect.Value.Index(i) 返回切片或数组第i个元素的reflect.Value值
reflect.Int()/reflect.Uint()/reflect.String()/reflect.Bool() 从反射的值对象中取出对应值,注意 reflect.Int()/reflect.Uint()方法对种类做了合并处理,它们只返回相应的最大范围的类型,Int()返回Int64类型,Uint()返回Uint64类型

获取 struct 反射值

go 复制代码
package main

import (
    "fmt"
    "reflect"
)

type Student struct {
    Name  string
    Age   int
    Score float64
}

func main() {

    st := Student{
       Name:  "zhangsan",
       Age:   18,
       Score: 95.5,
    }
    v := reflect.ValueOf(st)
    fmt.Printf("the field num of Student is %d\n", v.NumField())
    fmt.Printf("field1 type is %v, value is %s\n", v.Field(0).Type().Name(), v.Field(0).String())
    fmt.Printf("field2 type is %v, value is %d\n", v.Field(1).Type().Name(), v.Field(1).Int())
    fmt.Printf("field2 type is %v, value is %f\n", v.Field(2).Type().Name(), v.Field(2).Float())
}

运行结果:

go 复制代码
the field num of Student is 3
field1 type is string, value is zhangsan
field2 type is int, value is 18
field2 type is float64, value is 95.500000

v := reflect.ValueOf(st),v是一个 Student 类型的反射值对象,通过 v.NumField() 可以得出 Student 类型的字段个数,然后 v.Field(i).Type().Name() 打印出各个字段值的类型,v.Field(i) 打印出各个字段值

注意:NumField() 和 Field() 方法只有原对象是结构体时才能调用,否则会 panic

🦋 类型对象

函数/方法 说明
reflect.Type.NumField() 获取结构体的反射类型对象中的字段个数,只对结构体类型有效
reflect.Type.Field(i) 获取结构体的反射类型对象中的第i个字段,只对结构体类型有效
reflect.Type.Elem() 根据指针获取对应的具体类型
reflect.Type.NumIn() 获取函数反射类型的参数个数
reflect.Type.In(i) 获取函数反射类型的第i个参数
reflect.Type.NumOut() 获取函数反射类型的返回值个数
reflect.Type.Out(i) 获取函数反射类型的第i个返回值
reflect.Type.NumMethod() 获取struct上绑定的方法个数
reflect.Type.Method(i) 获取struct上绑定的第i个方法

struct 反射类型

go 复制代码
package main

import (
    "fmt"
    "reflect"
)

type Student struct {
    Name  string
    Age   int
    Score float64
}

func main() {
    st := Student{
       Name:  "zhangsan",
       Age:   18,
       Score: 90.5,
    }
    t := reflect.TypeOf(st)
    fmt.Println(t.Name())
    fmt.Println(t.Kind())
    fmt.Println(t.NumField())
    for i := 0; i < t.NumField(); i++ {
       fmt.Printf("field1 name is %s, field1 type is %s\n", t.Field(i).Name, t.Field(i).Type.String())
    }
}

运行结果:

go 复制代码
Student
struct
3
field1 name is Name, field1 type is string

通过 reflect.Type 的 Name() 方法可以获取对应的 Type 类型,Kind() 方法获取底层的数据种类,即 kind,跟 reflect.Value 一样,reflect.Type 也提供了 NumField() 方法用于获取结构体对象中的字段个数,通过 t.Field(i).Name 可以获取对应字段的名字。同样,Field(i) 和 NumField() 也只能对结构体反射使用

指针反射类型

go 复制代码
package main

import (
    "fmt"
    "reflect"
)

type Student struct {
    Name  string
    Age   int
    Score float64
}

func main() {
    st := &Student{
       Name:  "zhangsan",
       Age:   18,
       Score: 90.5,
    }
    t := reflect.TypeOf(st)

    fmt.Println(t.Kind())
    fmt.Println(t.Elem().Name())     // 这里一定要加Elem(),根据指针获取到具体类型后,才能或者具体的type名
    fmt.Println(t.Elem().NumField()) // 这里一定要加Elem(),根据指针获取到具体类型后,才能字段个数
    for i := 0; i < t.Elem().NumField(); i++ {
       fmt.Printf("field1 name is %s, field1 type is %s\n", t.Elem().Field(i).Name, t.Elem().Field(i).Type.String())
    }

}

运行结果:

go 复制代码
ptr
Student
3
field1 name is Name, field1 type is string
field2 name is Age, field2 type is int
field3 name is Score, field3 type is float64

可以看到,跟上面直接获取struct有一点点小小的区别,那就是fmt.Println(t.Kind())打印出的是一个ptr指针类型,而不再是struct类型,正是因为这里是一个ptr,所以我们不能直接在这个ptr上调用.Name()以及其他的.NumField()之类的方法,要根据ptr的.Elem()获取到具体类型之后,才能用这些方法,否则程序就回报panic,这点一定要注意

反射获取 struct 方法

go 复制代码
package main

import (
    "fmt"
    "reflect"
)

type Student struct {
    Name  string
    Age   int
    Score float64
}

func (s *Student) GetName() string {
    return s.Name
}

func (s *Student) SetName(name string) {
    s.Name = name
}

func (s *Student) GetAge() int {
    return s.Age
}

func (s *Student) SetAge(age int) {
    s.Age = age
}

func (s *Student) GetScore() float64 {
    return s.Score
}

func (s *Student) SetScore(score float64) {
    s.Score = score
}

func main() {
    st := &Student{
       Name:  "zhangsan",
       Age:   18,
       Score: 90.5,
    }
    t := reflect.TypeOf(st)

    for i := 0; i < t.NumMethod(); i++ {
       m := t.Method(i)
       fmt.Printf("%+v\n", m)
    }
}

运行结果:

go 复制代码
{GetName func(*main.Student) string}
{SetName func(*main.Student, string)}
{GetAge func(*main.Student) int}
{SetAge func(*main.Student, int)}
{GetScore func(*main.Student) float64}
{SetScore func(*main.Student, float64)}
  • reflect.Type.NumMethod():返回struct所绑定的的方法个数
  • reflect.Type.Method(i):返回第i个方法的 reflect.Method 对象

reflect.Method 定义在 src/reflect/type.go 文件:

go 复制代码
type Method struct {  
  Name    string // 方法名
  PkgPath string
  Type  Type  // 方法类型(
  Func  Value // 方法值(方法的接收器作为第一个参数)
  Index int   // 是结构体中的第几个方法
}

所以,通过 reflect.Method 对象,我们可以获取到 struct 所绑定的对应方法的方法名,方法类型等信息

通过反射调用方法

在上一小节我们知道了 reflect.Type.Method(i) 可以获取到 struct 所绑定的具体的方法对象 reflect.Method,通过这个对象,我们不仅可以获取方法的详细信息,还可以动态的调用方法。

其实在 reflect.Value 里我们也可以使用 NumMethod()/Method(i) 方法获取到对应的方法信息,不同的是 reflect.Value.Method(i) 返回的使一个 reflect.Value 对象,但是同样可以根据这个对象来动态调用方法,只是两者调用方法的方式有所区别

请看具体例子:

go 复制代码
package main

import (
    "fmt"
    "reflect"
)

type Student struct {
    Name  string
    Age   int
    Score float64
}

func (s *Student) GetName() string {
    return s.Name
}

func (s *Student) SetName(name string) {
    s.Name = name
}

func (s *Student) GetAge() int {
    return s.Age
}

func (s *Student) SetAge(age int) {
    s.Age = age
}

func (s *Student) GetScore() float64 {
    return s.Score
}

func (s *Student) SetScore(score float64) {
    s.Score = score
}

func main() {
    st := &Student{
       Name:  "zhangsan",
       Age:   18,
       Score: 90.5,
    }
    fmt.Printf("st === %+v\n", st)

    t := reflect.TypeOf(st)
    v := reflect.ValueOf(st)

    m1, ok := t.MethodByName("SetName")  // 获取SetName方法
    fmt.Printf("t get func by name:%t\n", ok)

    argsV1 := make([]reflect.Value, 0)
    argsV1 = append(argsV1, v)
    argsV1 = append(argsV1, reflect.ValueOf("lisi"))
    m1.Func.Call(argsV1)       // 
    fmt.Printf("st === %+v\n", st)

    m2 := v.MethodByName("SetName")    // 获取SetName方法
    argsV2 := make([]reflect.Value, 0)
    argsV2 = append(argsV2, reflect.ValueOf("wangwu"))
    m2.Call(argsV2)
    fmt.Printf("st === %+v\n", st)
}

运行结果:

go 复制代码
st === &{Name:zhangsan Age:18 Score:90.5}
t get func by name:true
st === &{Name:lisi Age:18 Score:90.5}
st === &{Name:wangwu Age:18 Score:90.5}

可以看到通过 reflect.Type.MethodByName() 方法获取到的reflect.Method对象和reflect.Value.MethodByName()方法获取到的reflect.Method获取到的reflect.Value对象都可以在程序运行时动态的调用方法修改结构本身,student的name由zhangsan------>lisi------>wangwu。

但是二者的调用存在一个区别:通过reflect.Method调用方法,必须使用Func字段,而且要传入接收器的reflect.Value作为第一个参数

go 复制代码
m1.Func.Call(argsV1)

reflect.Value.MethodByName()返回一个reflect.Value对象,它不需要接收器的reflect.Value作为第一个参数,而且直接使用Call()发起方法调用:

go 复制代码
m2.Call(argsV2)

通过反射设置值

go 复制代码
package main

import (
	"fmt"
	"reflect"
	"strings"
)

type User struct {
	Name1 string `big:"-"`
	Name2 string
}

func SetStruct(obj any) {
	v := reflect.ValueOf(obj).Elem()
	t := reflect.TypeOf(obj).Elem()
	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		bigField := field.Tag.Get("big")
		// 判断类型是不是字符串
		if field.Type.Kind() != reflect.String {
			continue
		}
		if bigField == "" {
			continue
		}
		// 修改值
		valueFiled := v.Field(i)
		valueFiled.SetString(strings.ToTitle(valueFiled.String()))
	}
}

func main() {
	s := User{Name1: "name1", Name2: "name2"}
	SetStruct(&s)
	fmt.Println(s)
}

四:🔥 反射的优缺点

优点:

  • 可以提升程序代码的灵活性,根据条件在程序运行时灵活的调用函数,并且修改源代码结构

缺点:

  • 主要是性能影响,反射过程中会有大量的内存开辟和 gc 过程,导致程序的性能降低

五:🔥 共勉

😋 以上就是我对 【Go】反射 的理解, 觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~ 😉

相关推荐
无奈何杨44 分钟前
业务如何对接风控决策,实时/异步,结果同步
前端·后端
郝学胜-神的一滴44 分钟前
Linux kill命令与kill函数:从信号原理到实战解析
linux·服务器·开发语言·c++·程序人生
未来之窗软件服务1 小时前
操作系统应用(三十七)C#华旭金卡身份证SDK-HX-FDX3S—东方仙盟筑基期
开发语言·c#·身份证阅读器·酒店管理系统·仙盟创梦ide
say_fall1 小时前
C语言编程实战:每日一题:有效的括号
c语言·开发语言·数据结构·
百锦再1 小时前
.NET到Java的终极迁移指南:最快转型路线图
android·java·开发语言·python·rust·go·.net
修一呀1 小时前
【阿里云ASR教程】阿里云一句话识别(NLS)实战:带 Token 缓存 + WAV 自动重采样的 Python 脚本
开发语言·python
1024小神1 小时前
使用AVFoundation实现二维码识别的角点坐标和区域
开发语言·数码相机·ios·swift
在坚持一下我可没意见1 小时前
Spring Boot 实战(一):拦截器 + 统一数据返回 + 统一异常处理,一站式搞定接口通用逻辑
java·服务器·spring boot·后端·spring·java-ee·tomcat
陌路201 小时前
C++ 单例模式
开发语言·c++