Golang 反射用法介绍

参考文章

Go 语言反射(reflection)简述

Go 语言反射规则浅析

深度解密 Go 语言之反射

Go 语言反射的实现原理

DeepEqual

Go语言inject库:依赖注入

浅谈控制反转与依赖注入

为什么要用反射

  1. 在 go1.19 版本之前,可以使用 反射 + interface 实现泛型的功能,可以极大的简化代码量。不过 golang 的泛型底层也是通过 反射 实现的。
  2. 当程序中需要根据用户的输入来决定调用对象时,就需要使用反射,使程序在运行期间动态地执行函数。

(举两个例子,可能会更好一些,之后找一下用反射最成功的例子。)

反射功能强大,但是也是有一些 弊端 的:

  1. 尽管使用反射可以增加代码复用性,但是使用反射会使得代码的可读性降低。
  2. 编译过程中,无法发现反射过程中的错误,所以很有可能当项目上线运行后,直接 panic,造成严重后果。
  3. 反射会使性能降低,比 正常代码的运行速度 会慢一到两个数量级。

所以:反射功能强大代码可读性以及性能并不理想,若非必要并不推荐使用反射。

反射是什么

维基百科定义

在计算机科学中, 反射是指计算机程序 在运行时可以 访问、检测和修改它本身状态或行为的一种能力。用比喻来说, 反射就是程序在 运行的时候能够“观察”并且修改自己的行为


提取关键句子:反射是指在程序运行期对程序本身进行访问和修改的能力


很强大么?很强大。原因如下:

没有反射时,对于静态语言来说,程序在编译的时候会将变量转化为内存地址,变量名并不会被编译器写入到可执行部分。所以,在运行过程中,就无法通过 变量名 获取自身的类型和值信息。
想要获取自身的相关信息,有且只有一个方法,通过内存地址获取。(太难了。。。。 )

反射的作用
使用反射在程序的编译期间就可以将变量的相关信息(如:类型和值信息等)整合到可执行文件中。
同时会提供反射接口让 程序 可以在运行期间访问到变量的相关信息。
最终,程序就可以在运行期间获取到变量的相关信息,并修改相关信息。

最后,再看一遍 Go 语言中对于反射的定义,就很容易去理解了:

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

反射(reflect)介绍

Go 语言通过官方提供的 reflect 包来访问程序的反射信息。

reflect 包中定义了 两个非常重要的类型:Type 和 Value。

Golang 中的任意类型 在反射中都可以被认为是由 reflect.Type 和 reflect.Value 组成的。

reflect 包中提供了 reflect.TypeOf 和 reflect.ValueOf 两个函数来获取 任意对象的 Type 和 Value。

  • reflect.TypeOf :获取对象的类型信息。
  • reflect.ValueOf :获取对象的值信息,甚至可以改变类型的值。

类型对象 reflect.Type

在 Go 语言中,使用 reflect.TypeOf() 函数获取任意值的类型对象(reflect.Type)。

通过该类型对象可以访问任意值的类型信息。

// 自定义一个 MyInt 类型
type MyInt int
// 声明一个空结构体
type MyStruct struct {}
func main() {
  // 定义一个变量
  var a MyInt
  typeOfA := reflect.TypeOf(a)
  // 显示反射类型对象的名称和种类 
  fmt.Println(typeOfA.Name(), typeOfA.Kind()) // MyInt int
  // 获取结构体实例的反射类型对象
  typeOfB := reflect.TypeOf(MyStruct{})
  // 显示反射类型对象的名称和种类
  fmt.Println(typeOfB.Name(), typeOfB.Kind()) // MyStruct struct
}

代码说明:

  • 第二行,自定义了一个 MyInt 类型。
  • 第四行,声明了一个空结构体。
  • 第八行,从上述代码中,可以看到,使用 TypeOf 函数生成的类型对象可以获取 类型信息。
    • Type.Name() 方法:返回自己定义的类型名称。
    • Type.Kind() 方法:返回 go 语言底层的类型名称。

例如:var a MyInt 中,它的类型对象的 Name:MyInt,Kind:int。

类型对象中,还有一个常用的方法是 Type.Elem(),它是用于获取 指针 所指向的值的类型的。

如以下代码所示:

// 自定义一个 MyInt 类型
type MyInt int
// 声明一个空结构体
type MyStruct struct {}
func main() { 
  // 获取结构体实例的反射类型对象
  typeOfB := reflect.TypeOf(&MyStruct{})
  // 显示反射类型对象的名称和种类
  fmt.Println(typeOfB.Name(), typeOfB.Kind()) // ptr
  fmt.Println(typeOfB.Elem(), typeOfB.Elem().Name(), typeOfB.Elem().Kind()) // model.MyStruct MyStruct struct
}

代码说明

  • 第二行,自定义了一个 MyInt 类型。
  • 第四行,声明了一个空结构体。
  • 第七行,获取结构体实例的反射类型对象。

当然,Type 的方法还有很多,如

  • MethodByName 获取当前类型对应方法的引用
  • Implements 判断当前类型是否实现了某个接口。等等。

当需要某些方法时,可以自行在源代码中寻找。

reflect.Type 被定义为一个接口,凡是它所定义的方法都可以调用,同时其中都有详细的介绍。

type Type interface {
  // Methods applicable to all types.
  // Align returns the alignment in bytes of a value of
  // this type when allocated in memory.
  Align() int
  // FieldAlign returns the alignment in bytes of a value of
  // this type when used as a field in a struct.
  FieldAlign() int
  // Method returns the i\'th method in the type\'s method set.
  // It panics if i is not in the range [0, NumMethod()).
  //
  // For a non-interface type T or *T, the returned Method\'s Type and Func
  // fields describe a function whose first argument is the receiver.
  //
  // For an interface type, the returned Method\'s Type field gives the
  // method signature, without a receiver, and the Func field is nil.
  //
  // Only exported methods are accessible and they are sorted in
  // lexicographic order.
  Method(int) Method
  // MethodByName returns the method with that name in the type\'s
  // method set and a boolean indicating if the method was found.
  //
  // For a non-interface type T or *T, the returned Method\'s Type and Func
  // fields describe a function whose first argument is the receiver.
  //
  // For an interface type, the returned Method\'s Type field gives the
  // method signature, without a receiver, and the Func field is nil.
  MethodByName(string) (Method, bool)
  // NumMethod returns the number of exported methods in the type\'s method set.
  NumMethod() int
  // Name returns the type\'s name within its package for a defined type.
  // For other (non-defined) types it returns the empty string.
  Name() string
  ...

值对象 reflect.Value

在 Go 语言中,使用 reflect.ValueOf() 函数获取任意变量的值对象(reflect.Type)。

通过该类型对象可以访问任意变量的值对象信息。

如果需要获取结构体中的变量信息,就需要使用 ValueOf 获取到结构体中的值,进而通过其他方法获取其他信息

不像 reflect.Type 类型是一个接口,reflect.Value 的类型被声明成了结构体。这个结构体没有对外暴露的字段,但是提供了获取或者写入数据的方法。

type Value struct {
  // 包含过滤的或者未导出的字段
}
func (v Value) Addr() Value
func (v Value) Bool() bool
func (v Value) Bytes() []byte
...


关于 reflect.Type 和 reflect.Value 的其他具体方法的使用,可以看接下来的反射三大法则

反射的三大法则

这是 Golang 官方博客中提供使用反射的三大法则。具体有如下三大法则

  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

接下来,开始一条一条的进行讲解

第一法则

Reflection goes from interface value to reflection object


这个很容易理解,讲的是:

反射可以将 接口类型(interface)变量 转化成 反射类型变量(指 reflect.Type 和 reflect.Value 类型)。而任意类型的变量都可以实现接口方法(Ducking Type),所以,任意类型的变量都可以转化成 反射类型的变量。

这个在之前的介绍中也提到了,接口类型变量 转化成 反射类型变量 主要是通过 reflect.TypeOf 和 reflect.ValueOf 两个方法。

通过这两个方法分别可以获取变量的类型和变量的值 ==(等价于) 获取了变量的全部信息。

func main() {
  var x float64 = 3.4
  fmt.Println("type:", reflect.TypeOf(x)) // type:float64
  fmt.Println("value:", reflect.ValueOf(x)) // value:3.4
}

第二法则

Reflection goes from reflection object to interface value


这个也容易理解,反射可以正向转化,也可以反向转化

反射可以将 反射类型对象 转化成 接口类型对象

反射类型对象 内部有一个方法(func Interface() interface {}),可以将自身对象转成 interface 类型的变量

那这么做,有什么好处呢?先看如下代码

func main() {
  a := 64.0
  v := reflect.ValueOf(a)
  fmt.Printf("type:%T\n", v)
  fmt.Println(v)
  fmt.Printf("type:%T\n", v.Interface())
  fmt.Println(v.Interface())
  fmt.Printf("value is %7.1e\n", v.Interface())
  // 打印输出:
  // type:reflect.Value
  // 64
  // type:float64
  // 64
  // value is 6.4e+01
}


可以看到:fmt.Println 的函数接收一个接口类型的变量。
而 v 的类型是 reflect.Value 类型的。
相当于在 fmt.Println 内部仍然还需要再转化一次,通过接口断言才能获取 v 的值。

而 v.Interface() 函数直接返回的就是 interface 类型对象。
该接口变量内部包含了具体值的类型信息,Printf 函数不需要做转化以及类型断言,就直接可以恢复类型信息。

总结:当然,我们在使用过程中,无论用不用 Interface() 方法,都能得出正确的结果。只不过无非就是资源消耗多少的问题。

第三法则

To modify a reflection object, the value must be settable

这个法则说的是,如果要修改 反射类型对象 ,其值必须是可设置的

“其值必须是可设置的”,指的是传值时,需要传指针类型(即引用类型)的。这也很容易理解。

Go 语言中函数调用值类型时,相当于重新复制一个值,所以得到的 反射对象 就与 变量 没有任何关系,那么直接修改 反射对象 无法改变原始变量。

如果是指针,就不存在如上问题,可以直接通过指针修改变量的值。

func main() {
  i := 1
  v := reflect.ValueOf(i)
  v.Elem().SetInt(10)
  fmt.Println(i)
}
// 上面的代码运行会报 panic 错误。
func main() {
  i := 1
  v := reflect.ValueOf(&i)
  v.Elem().SetInt(10)
  fmt.Println(i) // 10
}
// 这个程序则可以运行,发行 i 的值也改变了。

代码说明

  • 第 十 行,调用 reflect.ValueOf 获取变量指。
  • 第十一行,调用 Elem 方法获取指针指向的变量;然后调用 SetInt 方法更新变量的值。

反射功能

反射对于基本类型,可以根据以上三大法则对其进行使用,但是还有两个比较特殊的结构,需要另说一下,一个是结构体的反射,另一个是在反射中对函数的调用。

结构体反射

当使用反射对结构体中的数据进行修改时,需要保证两点:

一、需要结构体的指针(原因在第三法则时说了)。

二、需要结构体中的字段是可导出的(首字母需要大写)


这第二点,也很好理解。之前在接口那一节中也说过,结构体中的字段必须是可导出的(首字母大写),这样外部的包才能访问该字段。否则,它就是私有的,无法外部访问。只有能访问了,才有可能去修改:

// 声明一个结构体
type MyStruct struct {
  Name string
}

func main() {
  stu := MyStruct{Name: "Asa"} 
  // 获取结构体实例的反射类型对象
  typeOfB := reflect.ValueOf(&stu)
  // 显示反射类型对象的名称和种类
  s := typeOfB.Elem()
  s.Field(0).SetString("123")
  fmt.Println(typeOfB.Kind(), stu) // MyStruct struct
}

代码说明

  • 第 二 行,声明一个结构体
  • 第 七 行,将该结构体实例化
  • 第 九 行,获取结构体实例的反射类型对象
  • 第十一行,调用 Elem 方法获取指针指向的变量
  • 第十二行,使用Field方法获取第一个变量,然后调用 SetString 方法更新变量的值
  • 第十三行,打印。。。


代码中有两点需要注意:

Field 方法:结构体中一般包含一个或多个变量,这时就需要通过 Field 方法获取固定位置的变量。

Field 方法的功能介绍

Field returns the i'th field of the struct v.
Field 返回结构体 v 的第 i 个字段。
It panics if v's Kind is not Struct or i is out of range.
如果 v 的类型不是结构体,或者 i 超出了范围,则会报 panic。

setString 方法:使用反射修改变量的值时,需要根据变量的类型使用不同的set方法修改值。例如,这次 Name 的类型是 string,所以使用 setString 方法。

再往上的代码中,i 是int 类型,所以使用反射中的 setInt 方法修改值。如果使用的方法不对,程序运行时,将会报 panic 。

setString 方法的功能介绍

SetString sets v's underlying value to x
SetString将 v 的基础值设置为 x 。
It panics if v's Kind is not String or if CanSet() is false
如果 v 的类型不是 String 或者 CanSet() 为 false,则会产生panic。


那 CanSet() 方法又是什么呢?

它可以预先判断 传入的变量是否为指针变量;如果是结构体指针中的变量,还会判断该变量是否为可导出状态(首字母是否大写)

那为什么需要这个方法呢?如果不能修改的话,不是有报错提醒吗?

回答:对啊,是有报错提醒,但需要知道,那是程序运行时的报错(会让程序崩溃的),若只是编译不运行这个代码,将永远不会报错。而 CanSet() 可以在 set 之前做判断,如果不能修改,则打印输出,或者其他方法提醒管理员。而不会使程序报 panic ,导致程序崩溃。

函数调用

既然函数也实现了空接口方法,那么它也是可以成为 反射类型对象 的。


那么,如下代码则显示了如何通过 反射类型变量 调用函数的:

// 普通函数
func add(a, b int) int {
  return a + b
}
func main() {
  // 将函数包装为反射值对象
  funcValue := reflect.ValueOf(add)
  // 构造函数参数, 传入两个整型值
  paramList := []reflect.Value{reflect.ValueOf(10), reflect.ValueOf(20)}
  // 反射调用函数
  retList := funcValue.Call(paramList)
  // 获取第一个返回值, 取整数值
  fmt.Println(retList[0].Int())
}

代码说明如下

  • 第 2~4 行,定义一个普通的加法函数
  • 第 7 行,将 add 函数包装为反射值对象
  • 第 9 行,将 10 和 20 两个整型值使用 reflect.ValueOf 包装为 reflect.Value,再将反射值对象的切片 []reflect.Value 作为函数的参数 \n
  • 第 11 行,使用 funcValue 函数值对象的 Call() 方法,传入参数列表 paramList 调用 add() 函数
  • 第 13 行,调用成功后,通过 retList[0] 取返回值的第一个参数,使用 Int 取返回值的整数值

看完上述代码,就能很容易理解了 反射类型对象 调用函数的全过程

  1. 首先将函数转化为反射类型对象
  2. 然后将需要传入的参数也转化成一个反射类型对象的切片
  3. 最后将 带有参数的切片传入 call 方法中并会将结果以 反射类型对象 的类型返回,就完成了函数的调用

但是这个函数调用有什么用么?它能应用于什么场景? 应用于:inject 库中。

反射实际应用

(在实际生产中,见过写的超级优雅的反射案例,实现了类似于 泛型的功能,之后有机会再修改内容吧)

在反射的实际应用场景中,有两个很常见的操作,其内部都是调用的反射机制:Json 序列化,以及 DeepEqual 。

还有一个 inject 库是通过反射机制实现的依赖注入,可以了解以下。

Json序列

Json 序列化的主要用途就是将 结构体数据转化成 json 字符串数据,大多是用于服务器之间的通信,以及前后端的数据传输。

日常生活中最常用的方法有两种: 序列化 和 反序列化

序列化:将 结构体 转成 json 字符串

使用方法:json.Marshal(v interface{})([]byte, error)

应用场景

type student struct {
  name string
  Id int
  Score float64
}
func main() {
  stu := student{"asa", 123, 100}
  jsonStu, err := json.Marshal(stu)
  if err != nil {
    fmt.Println("json err")
  }
  fmt.Println(string(jsonStu)) // {"Id":123,"Score":100} 
}


代码说明

  • 第 一 行,定义了一个 student 结构体
  • 第 八 行,将 student 结构体实例化
  • 第 九 行,使用 json.Marshal 方法转化成 json 字节数组
  • 第 十三 行,将 json 字节数组 转化成 字符串,并输出

可以看到一个现象,在输出的 json 字符串中,并没有输出 name 属性。是因为 name 首字母没有大写,无法被外部的外部的方法捕获到。(正如上文讲结构体时也说过这一问题)


反序列化:将 json 字符串 填充到 结构体 \n \n 使用方法:json.Unmarshal(data []byte, v interface{}) erro

应用场景

type student struct {
 name string
 Id int
 Score float64
}

func main() {
 stuStr := "{"name":"asa","Id":123,"Score":100}"
 stu := student{}
 json.Unmarshal([]byte(stuStr), &stu)
 fmt.Println(stu) // { 123 100} 
}

代码说明

  • 第 一 行,定义了一个 student 结构体
  • 第 八 行,定义了一个 json 类型的字符串 stuStr
  • 第 九 行,实例化了一个空的 student 结构体
  • 第 十一 行,将 stuStr 字符串转化成 字节数组,然后使用 json.Unmarshal方法将其数据赋值给 stu
  • 第 十二 行,打印输出 stu 结构体中的数据

可以看到有两个奇特之处

  1. 打印输出中显示 name 为 空。原因同上
  2. 在 json.Unmarshal 传参时,传入的是 stu 的指针类型变量。原因也很好理解:可以参考本节第三法则进行理解

DeepEqual

DeepEqual 是反射中的一个方法,主要用于 Golang 中的深度比较。

当需要比较结构体中数据,或者切片,map 中的数据时,就可以使用这个方法。

在Golang 中,slice can only be compared to nil
type student struct {
  name string
  Id int
  Score float64
}
func main() {
  stu := student{"asa", 123, 100}
  stu2 := student{name: "asa", Score: 100,Id: 123}
  fmt.Println(reflect.DeepEqual(stu , stu2)) // true
  s1 := []int{1, 2, 3}
  s2 := []int{1, 2, 3}
  fmt.Println(reflect.DeepEqual(s1, s2)) // true
}

更多使用 DeepEqual 方法可以参考DeepEqual

Inject

这是一种第三方包,有许多大厂都实现了 Inject 库。在此主要介绍一下 inject 库 的用途,之后有用到的地方,可以自行去寻找是实现方法。

在介绍 inject 之前,先要了解 “依赖注入” 和 “控制反转” 概念。

控制反转(IOC Inversion Of Control):如果一个类A 的功能实现需要借助于类B,那么就称类B是类A的依赖,如果在类A的内部去实例化类B,那么两者之间会出现较高的耦合,一旦类B出现了问题,类A也需要进行改造,如果这样的情况较多,每个类之间都有很多依赖,那么就会出现牵一发而动全身的情况,程序会极难维护,并且很容易出现问题。要解决这个问题,就要把A类对B类的控制权抽离出来,交给一个第三方去做,把控制权反转给第三方,就称作控制反转(IOC Inversion Of Control)

依赖注入是实现控制反转的一种方法,如果说控制反转是一种设计思想,那么依赖注入就是这种思想的一种实现,通过注入参数或实例的方式实现控制反转。如果没有特殊说明,我们可以认为依赖注入和控制反转是一个东西。

控制反转的价值在于解耦,使用控制反转,就无需写许多的 init 结构体的方法,对于之后程序的维护和扩展有非常大的帮助。

读完以下这篇文章就很容易能理解了依赖注入的重要性

浅谈控制反转与依赖注入

END

还有反射源码分析,还要写么

之后再说吧 。。。。

请多多评论,有问题或者需要修改地方请指出来,我会及时回复的!!!

你可能感兴趣的:(golang,golang)