Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > Go反射原理

Go语言中的反射原理解析与应用

作者:景天科技苑

反射(Reflection)是计算机科学中的一个重要概念,它允许程序在运行时检查变量和值,获取它们的类型信息,并且能够修改它们,本文将结合实际案例,详细介绍Go语言中反射的基本概念、关键函数以及使用场景,需要的朋友可以参考下

引言

反射(Reflection)是计算机科学中的一个重要概念,它允许程序在运行时检查变量和值,获取它们的类型信息,并且能够修改它们。

Go语言通过内置的reflect包提供了反射功能,使得开发者可以编写灵活的代码,处理各种不同类型的值,而不必在编译时就知道这些值的具体类型。

本文将结合实际案例,详细介绍Go语言中反射的基本概念、关键函数以及使用场景。

一、反射的基本概念

在Go语言中,反射允许程序在运行时动态获取变量的各种信息,比如变量的类型、值等。
如果变量是结构体类型,还可以获取到结构体本身的各种信息,比如结构体的字段、方法。通过反射,还可以修改变量的值、调用方法。使用反射需要引入reflect包。

Go语言中的每一个变量都包含两部分信息:类型(type)和值(value)。reflect包让我们能够在运行时获取这些信息。
reflect.TypeOf()函数用于获取任何值的类型,返回一个reflect.Type类型的值。
reflect.ValueOf()函数用于获取任何值的运行时表示,返回一个reflect.Value类型的值。

二、静态类型与动态类型

Go语言是静态类型的语言,但是也可以通过反射机制实现一些动态语言的功能
在反射过程中,编译的时候就知道变量类型的就是静态类型、如果在运行时候才知道类型的就是动态类型

静态类型:变量在声明时候给他赋予类型的

var name string // string是静态类型的
var age int  // int 是静态类型的

动态类型:在运行的时候可能发生变化,主要考虑赋值问题

var A interface{}   // interface{} 静态类型 

A = 10              // interface{} 静态类型   此时的A是动态类型 int
A = "jingtian"   // interface{} 静态类型   此时的A是动态类型 string

像js,pyhton都是动态类型语言,定义变量的时候,不用指明类型,给它什么类型,运行时就是什么类型

在这里插入图片描述

在这里插入图片描述

三、为什么要用反射

1、我们需要编写一个函数,但是不知道函数传递给我的参数时什么?没约定好,传入的类型太多,这些类型不能统一表示,反射

2、我们在某些使用,需要根据条件来判断具体使用哪个函数处理问题,根据用户的输入来决定,这时候就需要对函数的参数进行反射,在运行期间来动态处理。

3、开发一些脚手架,框架的时候,自动实现一些底层的判断。比如 interface{} = any,由于这种动态类型是不确定的,我们可能在底层代码进行判断,从而选择使用什么来处理。

4、反射主要就是在不确定数据类型的和值的时候使用。如果我们不知道这个对象的信息,我们可以通过这个对象拿到代码中的一切。

四、为什么不建议使用反射

1、和反射相关的代码,不方便阅读,开发中,代码可读性(指标)很重要

2、Go语言是静态类型的语言,编译器可以找出开发时候的错误,如果代码中有大量反射代码,随时可能存在安全问题,panic,项目就终止

3、反射的性能很低,相对于正常的开发,至少慢2-3个数量级。项目关键位置低耗时,一定是不能使用反射的。更多时候使用约定。

五、反射的关键函数

1. reflect.Type 类型

reflect.Type是一个接口,通过reflect.TypeOf函数对接收的任意数据类型进行反射,可以获取该类型的各种信息。reflect.Type接口包含以下方法:

2. reflect.Value 类型

reflect.Value是一个结构体,为Go值提供了反射接口。reflect.Value包含以下方法:

六、反射的使用场景

1. 动态数据处理

反射的一个主要场景是处理动态数据结构,例如解析JSON或处理数据库查询结果。在这些场景中,数据的结构在编译时可能是未知的,因此需要使用反射来动态处理。

示例1:反射基本数据类型

package main

import (
    "fmt"
    "reflect"
)

// 反射
/*
Type : reflect.TypeOf(a) , 获取变量的类型
Value :reflect.ValueOf(a) , 获取变量的值
*/

func main() {
    // 正常编程定义变量
    var a int = 3
    // func TypeOf(i any) Type 反射得到该变量的数据类型
    fmt.Println("type", reflect.TypeOf(a))
    // func ValueOf(i any) Value  反射得到该变量的值
    fmt.Println("value", reflect.ValueOf(a))

    // 根据反射的值,来获取对象对应的类型和数值
    // 如果我们不知道这个对象的信息,我们可以通过这个对象拿到代码中的一切。
    // 获取a的类型
    v := reflect.ValueOf(a) // string int User
    // Kind : 获取这个值的种类, 在反射中,所有数据类型判断都是使用种类。
    //根据kind来判断其类型
    if v.Kind() == reflect.Float64 {
        fmt.Println(v.Float())
    }
    if v.Kind() == reflect.Int {
        fmt.Println(v.Int())
    }
    fmt.Println(v.Kind() == reflect.Float64)
    //fmt.Println(v.Type())

}

在这里插入图片描述

如果取值时,类型不对,会报panic异常

在这里插入图片描述

示例2:解析JSON

package main

import (
    "encoding/json"
    "fmt"
    "reflect"
)

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

func main() {
    jsonStr := `{"id":1, "name":"jigntian", "age":18}`
    var user User
    // func Unmarshal(data []byte, v any) error
    //先解析,看是否报错,要是报错,给的就不是json字符串
    err := json.Unmarshal([]byte(jsonStr), &user)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }

    // 使用反射获取User结构体的字段信息
    val := reflect.ValueOf(user)
    typ := reflect.TypeOf(user)

    for i := 0; i < val.NumField(); i++ {
        fieldVal := val.Field(i)
        fieldType := typ.Field(i)
        fmt.Printf("Field Name: %s, Field Value: %v, Field Type: %s\n", fieldType.Name, fieldVal, fieldType.Type)
    }
}

在这里插入图片描述

2. 获取类型信息

通过reflect.TypeOf函数,我们可以获取变量的类型信息。reflect.Type类型提供了多种方法来获取类型的详细信息,如类型名称、类型种类、字段信息等。

以下是一个示例,演示了如何获取类型信息:

package main

import (
    "fmt"
    "reflect"
)

type Person struct {
    Name string
    Age  int
}

func main() {
    var p Person = Person{Name: "Alice", Age: 30}

    // 获取类型信息
    t := reflect.TypeOf(p)

    // 打印类型名称
    fmt.Println("Type name:", t.Name())

    // 打印类型种类
    fmt.Println("Type kind:", t.Kind())

    // 打印结构体字段信息
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        fmt.Printf("Field name: %s, Field type: %s\n", field.Name, field.Type)
    }
}

在这里插入图片描述

在这个示例中,我们定义了一个Person结构体,并创建了一个Person类型的变量p。通过reflect.TypeOf函数,我们获取了变量p的类型信息。然后,我们打印了类型名称、类型种类以及结构体字段信息。

获取结构体信息

package main

import (
    "fmt"
    "reflect"
)

type User5 struct {
    Name string
    Age  int
    Sex  string
}

func (user User5) Say(msg string) {
    fmt.Println("User 说:", msg)
}

// PrintInfo  打印结构体信息
func (user User5) PrintInfo() {
    fmt.Printf("姓名:%s,年龄:%d,性别:%s\n", user.Name, user.Age, user.Sex)
}

func main() {
    user := User5{"jingtian", 18, "男"}

    reflectGetInfo(user)
}

// 通过反射,获取变量的信息
func reflectGetInfo(v interface{}) {
    // 1、获取参数的类型Type , 可能是用户自己定义的,但是Kind一定是内部类型struct
    getType := reflect.TypeOf(v)
    fmt.Println(getType.Name()) // 类型信息 User5
    fmt.Println(getType.Kind()) // 找到上级的种类Kind  struct

    // 2、获取值
    getValue := reflect.ValueOf(v)
    //查看类型 Type得到的是我们自定义的类型main.User5   Kind得到的是go内置的类型 struct
    fmt.Println("获取到的value--type值类型", getValue.Type()) // main.User5
    fmt.Println("获取到的value--kind值类型", getValue.Kind()) // struct
    fmt.Println("获取到value", getValue)

    // 获取字段,通过Type扒出字段
    // Type.NumField() 获取这个类型中有几个字段  3
    // field(index) 得到字段的值
    for i := 0; i < getType.NumField(); i++ {
        field := getType.Field(i) // 类型
        //通过valueof.Field().Interface拿到值
        value := getValue.Field(i).Interface() // value
        // 打印
        fmt.Printf("字段名:%s,字段类型:%s,字段值:%v\n", field.Name, field.Type, value)
    }

    // 获取这个结构体的方法 , NumMethod 可以获取方法的数量
    for i := 0; i < getType.NumMethod(); i++ {
        method := getType.Method(i)
        fmt.Printf("方法的名字:%s\t,方法类型:%s", method.Name, method.Type)
        //执行方法
        //method.Name()
    }

}

/*
由上面反射,就可以推导出结构体字段以及其方法
type User struct{
   Name string
   Age int
   Sex string
}
func (main.User) PrintInfo(){}
func (main.User) Say(string)
*/

在这里插入图片描述

通过反射可以 实现拿到一个对象,还原它的本身结构信息

3. 获取值信息

通过reflect.ValueOf函数,我们可以获取变量的值信息。reflect.Value类型提供了多种方法来获取和设置值,如获取布尔值、整数值、浮点数值、字符串值等。此外,reflect.Value类型还提供了方法来操作结构体字段、数组元素、映射键值对等。

以下是一个示例,演示了如何获取值信息并操作结构体字段:

package main

import (
    "fmt"
    "reflect"
)

type Person3 struct {
    Name string
    Age  int
}

func main() {
    var p Person3 = Person3{Name: "jingtian", Age: 18}

    // 获取值信息
    v := reflect.ValueOf(p)

    // 打印值信息
    fmt.Println("Value of Name:", v.FieldByName("Name").String())
    fmt.Println("Value of Age:", v.FieldByName("Age").Int())

    // 修改结构体字段值(注意:这里只是演示,实际上无法直接修改,因为v是不可变的)
    // 要修改值,需要使用reflect.ValueOf的Elem方法配合指针变量
    // 下面的代码会报错:panic: reflect: call of reflect.Value.SetInt on zero Value
    // v.FieldByName("Age").SetInt(35)

    // 正确的修改方式
    pv := reflect.ValueOf(&p).Elem() // 获取指针指向的元素的值
    pv.FieldByName("Age").SetInt(35) // 修改字段值

    // 打印修改后的值
    fmt.Println("Modified Age:", p.Age)
}

在这里插入图片描述

在这个示例中,我们定义了一个Person3结构体,并创建了一个Person3类型的变量p。通过reflect.ValueOf函数,我们获取了变量p的值信息。
然后,我们打印了结构体字段的值。注意,由于reflect.ValueOf返回的值是不可变的,所以我们不能直接修改字段值。为了修改字段值,我们需要使用reflect.ValueOf的Elem方法配合指针变量来获取可变的值。

4. 反射修改变量的值

通过反射修改值,需要操作对象的指针,拿到地址,然后拿到指针对象
通过Canset来判断值是否可以被修改

package main

import (
    "fmt"
    "reflect"
)

// 反射设置变量的值
func main() {

    var num float64 = 3.14
    update(&num)  //注意,这里传入指针地址
    fmt.Println(num)

}

func update(v any) {
    // 通过反射修改值,需要操作对象的指针,拿到地址,然后拿到指针对象
    pointer := reflect.ValueOf(v)
    newValue := pointer.Elem()

    fmt.Println("类型:", newValue.Type())
    fmt.Println("判断该类型是否可以修改:", newValue.CanSet())

    // 通过反射对象给变量赋值
    //newValue.SetFloat(999.43434)
    // 也可以对类型进行判断,根据不同类型修改成不同的值

    if newValue.Kind() == reflect.Float64 {
        // 通过反射对象给变量赋值
        newValue.SetFloat(2.21)
    }
    if newValue.Kind() == reflect.Int {
        // 通过反射对象给变量赋值
        newValue.SetInt(2)
    }
}

在这里插入图片描述

修改结构体变量:通过属性名,来实现修改

package main

import (
    "fmt"
    "reflect"
)

type Person4 struct {
    Name string
    Age  int
}

// 反射设置变量的值
func main() {

    person := Person4{"jingtian", 18}
    //注意,这里传入指针
    update2(&person)
    fmt.Println(person)

}

func update2(v any) {
    // 通过反射修改值,需要操作对象的指针,拿到地址,然后拿到指针对象
    pointer := reflect.ValueOf(v)
    newValue := pointer.Elem()

    fmt.Println("类型:", newValue.Type())
    fmt.Println("判断该类型是否可以修改:", newValue.CanSet())

    //修改结构体数据
    // 需要找到对象的结构体字段名
    newValue.FieldByName("Name").SetString("王安石")
        //如果不知道字段的类型,可以判断下
    fmt.Println("看下字段的类型", newValue.FieldByName("Name").Kind())

    newValue.FieldByName("Age").SetInt(99)

}

在这里插入图片描述

5. 反射调用方法

反射还可以用于在运行时动态调用对象的方法。这在需要根据字符串名称调用方法的场景下非常有用,例如实现一个简单的命令行接口或基于插件的架构。
通过方法名,找到这个方法, 然后调用 Call() 方法来执行 包括无参方法和有参方法

package main

import (
    "fmt"
    "reflect"
)

type User6 struct {
    Name string
    Age  int
    Sex  string
}

func (user User6) Say2(msg string) {
    fmt.Println(user.Name, "说:", msg)
}

// PrintInfo2  打印结构体信息
func (user User6) PrintInfo2() {
    fmt.Printf("姓名:%s,年龄:%d,性别:%s\n", user.Name, user.Age, user.Sex)
}

func main() {
    user := User6{"景天", 18, "男"}

    // 通过方法名,找到这个方法, 然后调用 Call() 方法来执行
    // 反射调用方法
    value := reflect.ValueOf(user)
    fmt.Printf("kind:%s,  type:%s\n", value.Kind(), value.Type())

    //根据方法名找到方法,通过call来调用方法,call里面跟参数,。无参使用nil
    // func (v Value) Call(in []Value) []Value  Call的参数是个reflect.Value类型的切片
    value.MethodByName("PrintInfo2").Call(nil) // 无参方法调用

    // 有参方法调用。先创建个reflect.Value类型的切片,长度为参数的个数
    args := make([]reflect.Value, 1)
    //给参数赋值
    // func ValueOf(i any) Value
    args[0] = reflect.ValueOf("这反射来调用的")
    //找到方法名,调用传参
    value.MethodByName("Say2").Call(args) // 有参方法调用
}

在这里插入图片描述

6. 反射调用函数

reflect.ValueOf 通过函数名来进行反射 得到函数名 reflect.ValueOf(函数名)
然后通过函数名.Call() 调用函数

package main

import (
    "fmt"
    "reflect"
)

// 反射调用函数  func
func main() {
    // 通过函数名来进行反射  reflect.ValueOf(函数名)
    // Kind func
    value1 := reflect.ValueOf(fun1)
    fmt.Println("打印拿到的数据类型", value1.Kind(), value1.Type())
    //调用无参函数
    value1.Call(nil)

    //调用有参函数
    value2 := reflect.ValueOf(fun2)
    fmt.Println("打印有参函数", value2.Kind(), value2.Type())

    //创建参数
    args1 := make([]reflect.Value, 2)
    args1[0] = reflect.ValueOf(1)
    args1[1] = reflect.ValueOf("hahahhaha")
    value2.Call(args1)

    //调用有参数,有返回值函数
    vuale3 := reflect.ValueOf(fun3)
    fmt.Println(vuale3.Kind(), vuale3.Type())
    args2 := make([]reflect.Value, 2)
    args2[0] = reflect.ValueOf(2)
    args2[1] = reflect.ValueOf("hahahhaha")
    //接收返回值 返回的是切片
    // func (v Value) Call(in []Value) []Value
    resultValue := vuale3.Call(args2)
    fmt.Println("返回值:", resultValue[0])
}

// 无参函数
func fun1() {
    fmt.Println("fun1:无参")
}

// 有参函数
func fun2(i int, s string) {
    fmt.Println("fun2:有参 i=", i, " s=", s)
}

// 有返回值函数
func fun3(i int, s string) string {
    fmt.Println("fun3:有参有返回值 i=", i, " s=", s)
    return s
}

在这里插入图片描述

七、反射的注意事项

性能开销:反射相对于直接操作类型有更高的性能开销,因为它需要在运行时进行类型检查和值转换。因此,在性能敏感的场景中应谨慎使用反射。

安全性:反射允许程序在运行时访问和修改几乎任何值,这可能导致意外的副作用或安全问题。因此,在使用反射时应确保只访问和修改预期的值,并避免潜在的类型冲突或数据损坏。

代码可读性:使用反射会使代码变得更加复杂和难以阅读。因此,在编写代码时应权衡反射带来的灵活性和代码可读性的重要性。

编译时检查:尽管反射提供了动态类型检查和值操作的能力,但它无法替代编译时类型检查。因此,在编写使用反射的代码时,应确保在编译时尽可能多地检查类型错误和逻辑错误。

八、总结

Go语言的反射功能提供了一种强大的机制来在运行时动态检查和操作值。通过反射,我们可以编写更加灵活和通用的代码来处理各种不同类型的值。然而,反射也带来了性能开销、安全性和代码可读性等挑战。因此,在使用反射时应谨慎权衡其优缺点,并根据具体场景做出合适的选择。

以上就是Go语言中的反射原理解析与应用的详细内容,更多关于Go反射原理的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文