Go语言的反射机制详解

反射是语言里面是非常重要的一个特性,我们经常会看见这个词,但是对于反射没有一个很好的理解,主要是因为对于反射的使用场景不太熟悉。

一、理解变量的内在机制

1.类型信息,元信息,是预先定义好的,静态的。

2.值信息,程序进行过程中,动态变化的。

二、反射和空接口

1.空接口相当于一个容器,能接受任何东西。

2.那怎么判断空接口变量存储的是什么类型呢?之前有使用过类型断言,这只是一个比较基础的方法

3.如果想获取存储变量的类型信息和值信息就要使用反射机制,所以反射是什么? 反射就是动态的获取变量类型信息和值信息的机制。

三、怎么利用反射分析空接口里面的信息呢?

①首先利用的是GO语言里面的Reflect包

②利用包里的TypeOf方法可以获取变量的类型信息

func reflect_typeof(a interface{}) {
    t := reflect.TypeOf(a)
    fmt.Printf("type of a is:%v\n", t)

    k := t.Kind()
    switch k {
    case reflect.Int64:
        fmt.Printf("a is int64\n")
    case reflect.String:
        fmt.Printf("a is string\n")
    }
}

利用Kind() 可以获取t的类型,如代码所示,这里可以判断a是Int64还是string, 像下面一样使用:

func main() {
    var x int64 = 3
    reflect_example(x)

    var y string = "hello"
    reflect_example(y)
}

打印结果:

type of a is:int64
a is int64
type of a is:string
a is string

③利用包里的ValueOf方法可以获取变量的值信息

func reflect_value(a interface{}) {
    v := reflect.ValueOf(a)
    k := v.Kind()
    switch k {
    case reflect.Int64:
        fmt.Printf("a is Int64, store value is:%d\n", v.Int())
    case reflect.String:
        fmt.Printf("a is String, store value is:%s\n", v.String())
    }
}

利用ValueOf方法可以得到变量的值信息,ValueOf返回的是一个Value结构体类型,有趣的是 可以使用 v.Type() 获取该变量的类型,和上面reflect.TypeOf() 获取的结果一样。

此外,因为值信息是动态的,所以我们不仅仅可以获取这个变量的类型,还能取得这个变量里面存储的值,利用 v.Int() 、 v.String() 等等就能取得值。如上面的main,调用此函数返回的结果:

a is Int64, store value is:3
a is String, store value is:hello

这里存在一个问题,如果传进去一个类型,使用了错误的解析,那么将会在运行的时候报错, 例如将 一个string类型强行的v.Int()。

既然值类型是动态的,能取到保存的值,同样可以设置值。在反射里面有很多set的方法,例如SetFloat、SetInt()、SetString()等可以帮助我们设置值。

下面的例子,我想把 x设置为 6.28,但是会报错!

func main() {
    var x float64 = 3.14
    v := reflect.ValueOf(x)
    v.SetFloat(6.28)
    fmt.Printf("After Set Value is %f", x)
}

错误结果:

panic: reflect: reflect.Value.SetFloat using unaddressable value
......

结果上说明是不可设置的,为什么呢? 因为我们的x是一个值类型,而值类型的传递是拷贝了一个副本,当 v := reflect.ValueOf(x) 函数通过传递一个 x 拷贝创建了 v,那么 v 的改变并不能更改原始的 x。要想 v 的更改能作用到 x,那就必须传递 x 的地址 v = reflect.ValueOf(&x)。修改程序如下:

func main() {
    var x float64 = 3.14
    v := reflect.ValueOf(&x)
    v.SetFloat(6.28)
    fmt.Printf("After Set Value is %f", x)
}

结果:依然报错!为什么传了地址还报错?因为&x是地址了,所以它的类型就变了,可以通过v.Type(),看下改变成了什么:

func main() {
    var x float64 = 3.14
    v := reflect.ValueOf(&x)
    fmt.Printf("type of v is %v", v.Type())   //打印的结果是:type of v is *float64
}

由程序可以知道,这个返回的是一个指针类型的。所以上面SetFloat才会失败,那怎么做?

我们正常的赋值,如果是地址的话,例如下面:一般我们都会对*y进行赋值, *的意思就是往这个地址里面赋值。

var y *float64 = new(float64)
*y = 10.12
fmt.Printf("y = %v", *y)

同样的,我们在反射里面也可以取地址,需要通过 Elem() 方法进行取地址。再次修改程序

func main() {
    var x float64 = 3.14
    v := reflect.ValueOf(&x)
    fmt.Printf("type of v is %v\n", v.Type())
    v.Elem().SetFloat(6.28)
    fmt.Printf("After set x is %v", x)
}

结果为:

type of v is *float64
After set x is 6.28

四、利用反射获取结构体里面的方法和调用。

1.获取结构体的字段

我们可以通过上面的方法判断一个变量是不是结构体。

可以通过 NumField() 获取所有结构体字段的数目、进而遍历,通过Field()方法获取字段的信息。

type Student struct {
    Name  string
    Sex   int
    Age   int
    Score float32
}

func main() {
    //创建一个结构体变量
    var s Student = Student{
        Name:  "BigOrange",
        Sex:   1,
        Age:   10,
        Score: 80.1,
    }

    v := reflect.ValueOf(s)
    t := v.Type()
    kind := t.Kind()

    //分析s变量的类型,如果是结构体类型,那么遍历所有的字段
    switch kind {
    case reflect.Int64:
        fmt.Printf("s is int64\n")
    case reflect.Float32:
        fmt.Printf("s is int64\n")
    case reflect.Struct:
        fmt.Printf("s is struct\n")
        fmt.Printf("field num of s is %d\n", v.NumField())
        //NumFiled()获取字段数,v.Field(i)可以取得下标位置的字段信息,返回的是一个Value类型的值
        for i := 0; i < v.NumField(); i++ {
            field := v.Field(i)
            //打印字段的名称、类型以及值
            fmt.Printf("name:%s type:%v value:%v\n",
                t.Field(i).Name, field.Type().Kind(), field.Interface())
        }
    default:
        fmt.Printf("default\n")
    }
}

执行结果:

s is struct
field num of s is 4
name:Name type:string value:BigOrange
name:Sex type:int value:1
name:Age type:int value:10
name:Score type:float32 value:80.1

这里需要说明几个问题:

①打印字段名称的时候,使用的是t.Field(i).Name ,Name是静态的,所以属于类型的信息

②打印值的时候,这里将field.Interface()实际上相当于ValueOf的反操作(可以参考这篇文章https://www.jb51.net/article/255856.htm),所以才能把值打印出来

③此外如果Student中的Name字段变为name(私有),那么则会报错,不能反射出私有变量 错误信息 “panic: reflect.Value.Interface: cannot return value obtained from unexported field or method”

2.对结构体内的字段进行赋值操作

参考下面的代码,对上面的Student进行赋值操作:

func main() {
    s := Student{
        Name:  "BigOrange",
        Sex:   1,
        Age:   10,
        Score: 80.1,
    }

    fmt.Printf("Name:%v, Sex:%v,Age:%v,Score:%v \n", s.Name, s.Sex, s.Age, s.Score)
    v := reflect.ValueOf(&s)  //这里传的是地址!!!

    v.Elem().Field(0).SetString("ChangeName")
    v.Elem().FieldByName("Score").SetFloat(99.9)

    fmt.Printf("Name:%v, Sex:%v,Age:%v,Score:%v \n", s.Name, s.Sex, s.Age, s.Score)
}

结果:

Name:BigOrange, Sex:1,Age:10,Score:80.1
Name:ChangeName, Sex:1,Age:10,Score:99.9

3.获取结构体里面的方法

可以通过NumMethod()获得接头体里面的方法数量,然后遍历通过Method()获取方法的具体信息。如下代码所示:

//新增-设置名称方法
func (s *Student) SetName(name string) {
     fmt.Printf("有参数方法 通过反射进行调用:%v\n", s)
     s.Name = name
}
//新增-打印信息方法
func (s *Student) PrintStudent() {
    fmt.Printf("无参数方法 通过反射进行调用:%v\n", s)
}

func main() {
    s := Student{
        Name:  "BigOrange",
        Sex:   1,
        Age:   10,
        Score: 80.1,
    }

    v := reflect.ValueOf(&s)
    //取得Type信息
    t := v.Type()

    fmt.Printf("struct student have %d methods\n", t.NumMethod())

    for i := 0; i < t.NumMethod(); i++ {
        method := t.Method(i)
        fmt.Printf("struct %d method, name:%s type:%v\n", i, method.Name, method.Type)
    }
}

输出:

struct student have 2 methods
struct 0 method, name:PrintStudent type:func(*main.Student)
struct 1 method, name:SetName type:func(*main.Student, string)

从结果中看到我们可以获取方法的名称以及签名信息,并且这个方法的输出顺序是按照字母排列的。

并且输出结果可以看到一个有趣的现象:结构体的方法其实也是通过函数实现的例如 func(s Student) SetName(name string) 这个方法,反射之后的结果就是 func(main.Student , string) 实际上把Student当参数了。

此外还可以通过反射来调用这些方法。想要通过反射调用结构体里面的方法,首先要知道方法调用时一个动态的,所以要先通过ValueOf获取值,然后通过获取的值进行方法的调用 ,通过 value里面的Method方法 返回一个方法,然后通过Call方法调用,Call是参数是一个切片,也就是参数的列表。以下是Call方法的定义:可以看到参数是一个Value的数组:

如下代码展示了如何调用有参数的方法和无参数的方法:

func main() {
    s := Student{
        Name:  "BigOrange",
        Sex:   1,
        Age:   10,
        Score: 80.1,
    }

    v := reflect.ValueOf(&s)

    //通过reflect.Value获取对应的方法并调用
    m1 := v.MethodByName("PrintStudent")
    var args []reflect.Value
    m1.Call(args)

    m2 := v.MethodByName("SetName")
    var args2 []reflect.Value
    name := "stu01"
    nameVal := reflect.ValueOf(name)
    args2 = append(args2, nameVal)
    m2.Call(args2)
    m1.Call(args)
}

执行结果:

无参数方法 通过反射进行调用:&main.Student{Name:"BigOrange", Sex:1, Age:10, Score:80.1}
有参数方法 通过反射进行调用:&main.Student{Name:"BigOrange", Sex:1, Age:10, Score:80.1}
无参数方法 通过反射进行调用:&main.Student{Name:"stu01", Sex:1, Age:10, Score:80.1}

上面格式打印:

  • %v 相应值的默认格式。 Printf("%v", people) {zhangsan},
  • %+v 打印结构体时,会添加字段名 Printf("%+v", people) {Name:zhangsan}
  • %#v 相应值的Go语法表示 Printf("#v", people) main.Human{Name:"zhangsan"}

五、怎么获取结构体里tag的信息。

有时候我们在类型上面定义一些tag,例如使用json和数据库的时候。Field()方法返回的StructField结构体中保存着Tag信息,并且Tag信息可以通过一个Get(Key)的方法获取出来,如下代码所示:

type Student struct {
    Name string `json:"jsName" db:"dbName"`
}

func main() {
    s := Student{
        Name: "BigOrange",
    }
    v := reflect.ValueOf(&s)
    t := v.Type()
    field0 := t.Elem().Field(0)
    fmt.Printf("tag json=%s\n", field0.Tag.Get("json"))
    fmt.Printf("tag db=%s\n", field0.Tag.Get("db"))
}

结果:

tag json=jsName
tag db=dbName

六、应用场景

1.序列化和反序列化,比如json, protobuf等各种数据协议

2.各种数据库的ORM,比如gorm,sqlx等数据库中间件

3.配置文件解析相关的库,比如yaml、ini等

到此这篇关于Go语言反射机制的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Go语言基础反射示例详解

    目录 概述 语法 一.基本操作 二.修改目标对象 三.动态调用方法 总结 示例 概述 在程序运行期对程序动态的进行访问和修改 reflect godoc: https://golang.org/pkg/reflect/ reflect包有两个数据类型: Type:数据类型 [reflect.TypeOf():是获取Type的方法] Value:值的类型[reflect.ValueOf():是获取Value的方法] 语法 一.基本操作 获取变量类型 func TypeOf(i interface{

  • Go语言学习之反射的用法详解

    目录 1. reflect 包 1.1 获取变量类型 1.2 断言处理类型转换 2. ValueOf 2.1 获取变量值 2.2 类型转换 3. Value.Set 3.1 设置变量值 3.2 示例 4. 结构体反射 4.1 查看结构体字段数量和方法数量 4.2 获取结构体属性 4.3 更改属性值 4.4 Tag原信息处理 5. 函数反射 6. 方法反射 6.1 使用 MethodByName 名称调用方法 6.2 使用 method 索引调用方法 反射指的是运行时动态的获取变量的相关信息 1.

  • Go系列教程之反射的用法

    反射是 Go 语言的高级主题之一.我会尽可能让它变得简单易懂. 本教程分为如下小节. 什么是反射? 为何需要检查变量,确定变量的类型? reflect 包 reflect.Type 和 reflect.Value reflect.Kind NumField() 和 Field() 方法 Int() 和 String() 方法 完整的程序 我们应该使用反射吗? 让我们来逐个讨论这些章节. 什么是反射? 反射就是程序能够在运行时检查变量和值,求出它们的类型.你可能还不太懂,这没关系.在本教程结束后,

  • golang 如何通过反射创建新对象

    废话少说,直接贴代码~ type A struct { Name string } // 测试unit func TestReflect(t *testing.T) { reflectNew((*A)(nil)) } //反射创建新对象. func reflectNew(target interface{}) { if target == nil { fmt.Println("参数不能未空") return } t := reflect.TypeOf(target) if t.Kind

  • Go语言中反射的正确使用

    介绍 反射是元数据编程的一种形式,指的是程序获得本身结构的一种能力.不同语言的反射模型实现不一样,本文中的反射,仅仅指的是Go语言中的反射模型. 反射有两个问题,在使用前需要三思: 大量的使用反射会损失一定性能 Clear is better than clever. Reflection is never clear. Go的类型设计上有一些基本原则,理解这些基本原则会有助于你理解反射的本质: 变量包括 <type, value> 两部分.理解这一点你就知道为什么nil != nil了. t

  • Golang学习之反射机制的用法详解

    目录 介绍 TypeOf() ValueOf() 获取接口变量信息 事先知道原有类型的时候 事先不知道原有类型的时候 介绍 反射的本质就是在程序运行的时候,获取对象的类型信息和内存结构,反射是把双刃剑,功能强大但可读性差,反射代码无法在编译阶段静态发现错误,反射的代码常常比正常代码效率低1~2个数量级,如果在关键位置使用反射会直接导致代码效率问题,所以,如非必要,不建议使用. 静态类型是指在编译的时候就能确定的类型(常见的变量声明类型都是静态类型):动态类型是指在运行的时候才能确定的类型(比如接

  • golang通过反射设置结构体变量的值

    如果需要动态设置struct变量field的情况下, 可以利用reflect来完成. 代码如下: package main import ( "fmt" "reflect" ) // 定义结构体Person type Person struct { Name string Age int } func main() { person := Person{} fmt.Println(person) // 修改前 { 0} pp := reflect.ValueOf(&

  • Go语言中使用反射的方法

    本文实例讲述了Go语言中使用反射的方法.分享给大家供大家参考.具体实现方法如下: 复制代码 代码如下: // Data Model type Dish struct {   Id  int   Name string   Origin string   Query func() } 创建实例如下: 复制代码 代码如下: shabushabu = Dish.new shabushabu.instance_variables # => [] shabushabu.name = "Shabu-S

  • Go语言反射获取类型属性和方法示例

    本系列文章,我将会进一步加深对 Go 语言的讲解,更一步介绍 Go 中的包管理.反射和并发等高级特性. 前面一篇文章主要介绍了 reflect.Type 类型对象.本文将会继续介绍 Go 反射 reflect.StructField 和 reflect.Method 相关的内容. reflect.StructField 和 reflect.Method 如果变量是一个结构体,我们还可以通过结构体域类型对象 reflect.StructField 来获取结构体下字段的类型属性.Type 接口下提供

  • 图文详解go语言反射实现原理

    Go反射的实现和 interface 和 unsafe.Pointer 密切相关.如果对golang的 interface 底层实现还没有理解,可以去看我之前的文章: Go语言interface底层实现 , unsafe.Pointer 会在后续的文章中做介绍. (本文目前使用的Go环境是Go 1.12.9) interface回顾 首先我们简单的回顾一下interface的结构,总体上是: 细分下来分为有函数的 iface 和无函数的 eface (就是 interface{} ); 无函数的

  • Go语言学习笔记之反射用法详解

    本文实例讲述了Go学习笔记之反射用法.分享给大家供大家参考,具体如下: 一.类型(Type) 反射(reflect)让我们能在运行期探知对象的类型信息和内存结构,这从一定程度上弥(mi)补了静态语言在动态行为上的不足.同时,反射还是实现元编程的重要手段. 和 C 数据结构一样,Go 对象头部并没有类型指针,通过其自身是无法在运行期获知任何类型相关信息的.反射操作所需要的全部信息都源自接口变量.接口变量除存储自身类型外,还会保存实际对象的类型数据. func TypeOf(i interface{

  • 学习使用Go反射的用法示例

    什么是反射 大多数时候,Go中的变量,类型和函数非常简单直接.当需要一个类型.变量或者是函数时,可以直接定义它们: type Foo struct { A int B string } var x Foo func DoSomething(f Foo) { fmt.Println(f.A, f.B) } 但是有时你希望在运行时使用变量的在编写程序时还不存在的信息.比如你正在尝试将文件或网络请求中的数据映射到变量中.或者你想构建一个适用于不同类型的工具.在这种情况下,你需要使用反射.反射使您能够在

随机推荐