反射 - Go 语言学习笔记

前言

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

什么是 Go 的反射

Go 语言提供了一种机制在运行时更新变量和检查它们的值、调用它们的方法,可是在编译时并不知道这些变量的具体类型,这称为反射机制(refletion)。函数

Go 语言官方自带的 reflect 包就是实现反射相关的,reflect 包定义了各类类型,实现了反射的各类函数,经过它们能够在运行时检测类型的信息、改变类型的值。工具

为何须要反射

须要反射的 2 个常见场景:布局

  1. 若是要编写一个函数,用于处理通用类型的值,而这些类型可能没法共享同一个接口,也可能布局未知,也有可能这个类型在咱们设计函数时还不存在,甚至这个类型会同时存在这三种问题,这时,反射是最好的解决方案。
  2. 有时候须要根据某些条件决定调用哪一个函数,好比根据用户的输入来决定。这时就须要对函数和函数的参数进行反射,在运行期间动态地执行函数。

Go 语言的 fmt.Printf 函数中的格式化逻辑就是使用反射处理相似以上存在的问题来实现的。测试

下面尝试实现一个相似 fmt.Printf 功能的函数,为了简单起见,函数只接收一个参数,而后返回和 fmt.Sprint 相似的格式化后的字符串,函数名也叫 Sprint。ui

首先用 switch 类型分支来测试输入参数是否实现了 String 方法,若是是就调用该方法。而后继续增长类型测试分支,检查这个值的动态类型是不是 string、int、bool 等基础类型,并在每种状况下执行相应的格式化操做。url

func Sprint(x interface{}) string {
    type stringer interface {
        String() string
    }
    switch x := x.(type) {
    case stringer:
        return x.String()
    case string:
        return x
    case int:
        return strconv.Itoa(x)
    // ...similar cases for int16, uint32, and so on...
    case bool:
        if x {
            return "true"
        }
        return "false"
    default:
        // array, chan, func, map, pointer, slice, struct
        return "???"
    }
}
复制代码

以上函数虽然实现了部分类型的格式化输出,可是如何处理其它相似 []float6四、map[string][]string 等类型呢?固然能够添加更多的测试分支,可是这些组合类型的数目基本是无穷的。还有如何处理相似url.Values这样的具名类型呢?即便类型分支能够识别出底层的基础类型是 map[string][]string,可是它并不匹配 url.Values 类型,由于它们是两种不一样的类型,并且 switch 类型分支也不可能包含每一个相似 url.Values 的类型,这会致使对这些库的依赖。spa

没有办法来检查未知类型的表示方式,被卡住了。这就是为什么须要反射的缘由。设计

反射是如何实现的

interface 是 Go 语言实现抽象的一个很是强大的工具。当向接口变量赋予一个实体类型的时候,接口会存储实体的类型信息,反射就是经过接口的类型信息实现的,反射创建在类型的基础上。指针

types 和 interface

Go 语言关于类型设计的一些原则:

  • 变量包括 type, value 这两部分。其中 type 包括 static type 和 concrete type,static type 是在编写程序时看见的类型(即变量声明时赋予的类型,如int、string),concrete type 是 runtime 系统时看见的类型(即运行时给这个变量赋值后,该变量的类型)。

  • 类型断言可否成功,取决于变量的 concrete type,而不是 static type。因此,一个 reader 变量若是它的 concrete type 也实现了 write 方法,它能够被类型断言为 writer。

反射创建在类型之上,Go 语言声明变量时指定的 type 是 static type,在建立变量的时候就已经肯定。反射主要与 Go 语言的 interface 类型相关(它的 type 是 concrete type ),只有 interface 类型才有反射一说。

Go 语言中,每一个 interface 变量都有一个对应 pair,pair 中记录了实际变量的值和类型:

(value, type)
复制代码

以上,value 是实际变量值,type 是实际变量的类型。一个 interface{} 类型的变量包含了2个指针,一个指针指向值的类型【对应 concrete type】,另一个指针指向实际的值【对应 value】。

Go 语言的 reflect 包

Go 语言的反射功能由 reflect 包提供,它实现了运行时反射,使用它能识别 interface{} 变量的底层具体类型和具体值。

1. reflect.Type 和 reflect.Value
reflect 包定义了两个重要的类型:Type 和 Value。reflect.Type 表示 interface{} 的具体类型,而 reflect.Value 表示它的具体值。reflect.TypeOf() 和 reflect.ValueOf() 两个函数能够分别返回 reflect.Type 和 reflect.Value。

  • TypeOf 用来动态获取输入参数接口中的值的类型,若是接口为空则返回nil
  • ValueOf用来获取输入参数接口中的数据的值,若是接口为空则返回0

即,reflect.TypeOf() 是获取 pair 中的 type,reflect.ValueOf() 获取 pair 中的value,示例以下:

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var num float64 = 1.2345

	fmt.Println("type: ", reflect.TypeOf(num))
	fmt.Println("value: ", reflect.ValueOf(num))
}
复制代码

运行结果:

type:  float64
value:  1.2345
复制代码

2. relfect.Kind
reflect 包中还有一个重要的类型:Kind。 在反射包中,Kind 和 Type 的类型可能看起来很类似,但在下面程序中,能够很清楚地看出它们的不一样之处。

package main

import (
    "fmt"
    "reflect"
)

type order struct {
    ordId      int
    customerId int
}

func createQuery(q interface{}) {
    t := reflect.TypeOf(q)
    k := t.Kind()
    fmt.Println("Type ", t)
    fmt.Println("Kind ", k)


}

func main() {
    o := order{
        ordId:      456,
        customerId: 56,
    }
    createQuery(o)
}
复制代码

输出:

Type  main.order
Kind  struct
复制代码

由上可知:Type 表示 interface{} 的实际类型(在这里是 main.Order),而 Kind 表示该类型的特定类别(在这里是 struct)。

反射的使用

1. 从 relfect.Value 中获取接口 interface 的信息

当执行 reflect.ValueOf(interface) 以后,就获得了一个类型为 “relfect.Value” 变量,能够经过它自己的 Interface() 方法得到接口变量的真实内容,而后能够经过类型判断进行转换,转换为原有真实类型。

  • 已知原有类型【进行“强制转换”】
    已知类型后转换为其对应的类型的作法以下,直接经过 Interface 方法而后强制转换,以下:
realValue := value.Interface().(已知的类型)
复制代码

示例以下:

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var num float64 = 1.2345

	pointer := reflect.ValueOf(&num)
	value := reflect.ValueOf(num)

	// 能够理解为“强制转换”,可是须要注意的时候,转换的时候,若是转换的类型不彻底符合,则直接panic
	// Golang 对类型要求很是严格,类型必定要彻底符合
	// 以下两个,一个是*float64,一个是float64,若是弄混,则会panic
	convertPointer := pointer.Interface().(*float64)
	convertValue := value.Interface().(float64)

	fmt.Println(convertPointer)
	fmt.Println(convertValue)
}
复制代码

运行结果:

0xc42000e238
1.2345
复制代码

说明:

  1. 转换的时候,若是转换的类型不彻底符合,则直接panic,类型要求很是严格!
  2. 转换的时候,要区分是指针仍是指
  3. 也就是说反射能够将“反射类型对象”再从新转换为“接口类型变量”
  • 未知原有类型【遍历探测其Filed】】
    不少状况下,可能并不知道其具体类型,那么这个时候,须要进行遍历探测其 Filed 来得知,示例以下:
package main

import (
	"fmt"
	"reflect"
)

type User struct {
	Id   int
	Name string
	Age  int
}

func main() {

	user := User{1, "Allen.Wu", 25}

	DoFiledAndMethod(user)

}

// 经过接口来获取任意参数,而后一一揭晓
func DoFiledAndMethod(input interface{}) {

	getType := reflect.TypeOf(input)
	fmt.Println("get Type is :", getType.Name())

	getValue := reflect.ValueOf(input)
	fmt.Println("get all Fields is:", getValue)

	// 获取方法字段
	// 1. 先获取interface的reflect.Type,而后经过NumField进行遍历
	// 2. 再经过reflect.Type的Field获取其Field
	// 3. 最后经过Field的Interface()获得对应的value
	for i := 0; i < getType.NumField(); i++ {
		field := getType.Field(i)
		value := getValue.Field(i).Interface()
		fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)
	}

	// 获取方法
	// 1. 先获取interface的reflect.Type,而后经过.NumMethod进行遍历
	for i := 0; i < getType.NumMethod(); i++ {
		m := getType.Method(i)
		fmt.Printf("%s: %v\n", m.Name, m.Type)
	}
}
复制代码

运行结果:

get Type is : User
get all Fields is: {1 Allen.Wu 25}
Id: int = 1
Name: string = Allen.Wu
Age: int = 25
ReflectCallFunc: func(main.User)
复制代码

说明
经过运行结果能够得知获取未知类型的 interface 的具体变量及其类型的步骤为:

  1. 先获取 interface 的 reflect.Type,而后经过 NumField 进行遍历
  2. 再经过 reflect.Type 的 Field 获取其 Field
  3. 最后经过 Field 的 Interface() 获得对应的 value

经过运行结果能够得知获取未知类型的interface的所属方法(函数)的步骤为:

  1. 先获取 interface 的 reflect.Type,而后经过 NumMethod 进行遍历
  2. 再分别经过 reflect.Type 的 Method 获取对应的真实的方法(函数)
  3. 最后对结果取其 Name 和 Type 得知具体的方法名
  4. 也就是说反射能够将“反射类型对象”再从新转换为“接口类型变量”
  5. struct 或者 struct 的嵌套都是同样的判断处理方式

2. 经过 reflect.Value 设置实际变量的值

reflect.Value 是经过 reflect.ValueOf(x) 得到的,只有当 x 是指针的时候,才能够经过 reflec.Value 修改实际变量 x 的值,即:要修改反射类型的对象就必定要保证其值是 “addressable” 的。 示例以下:

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var num float64 = 1.2345
	fmt.Println("old value of pointer:", num)
	
	// 经过reflect.ValueOf获取num中的reflect.Value,注意,参数必须是指针才能修改其值
	pointer := reflect.ValueOf(&num)
	newValue := pointer.Elem()
	
	fmt.Println("type of pointer:", newValue.Type())
	fmt.Println("settability of pointer:", newValue.CanSet())
	
	// 从新赋值
	newValue.SetFloat(77)
	fmt.Println("new value of pointer:", num)
	
	// 若是reflect.ValueOf的参数不是指针,会如何?
	pointer = reflect.ValueOf(num)
	//newValue = pointer.Elem() // 若是非指针,这里直接panic,“panic: reflect: call of reflect.Value.Elem on float64 Value”
}
复制代码

运行结果:

old value of pointer: 1.2345
type of pointer: float64
settability of pointer: true
new value of pointer: 77
复制代码

说明

  1. 须要传入的参数是* float64这个指针,而后能够经过pointer.Elem()去获取所指向的Value,注意必定要是指针。
  2. 若是传入的参数不是指针,而是变量,那么
    • 经过Elem获取原始值对应的对象则直接panic
    • 经过CanSet方法查询是否能够设置返回false
  3. newValue.CantSet()表示是否能够从新设置其值,若是输出的是true则可修改,不然不能修改,修改完以后再进行打印发现真的已经修改了。
  4. reflect.Value.Elem() 表示获取原始值对应的反射对象,只有原始对象才能修改,当前反射对象是不能修改的
  5. 也就是说若是要修改反射类型对象,其值必须是“addressable”【对应的要传入的是指针,同时要经过Elem方法获取原始值对应的反射对象】
  6. struct 或者 struct 的嵌套都是同样的判断处理方式

3. 经过 reflect.ValueOf 来进行方法的调用

示例以下:

package main

import (
	"fmt"
	"reflect"
)

type User struct {
	Id   int
	Name string
	Age  int
}

func (u User) ReflectCallFuncHasArgs(name string, age int) {
	fmt.Println("ReflectCallFuncHasArgs name: ", name, ", age:", age, "and origal User.Name:", u.Name)
}

func (u User) ReflectCallFuncNoArgs() {
	fmt.Println("ReflectCallFuncNoArgs")
}

// 如何经过反射来进行方法的调用?
// 原本能够用u.ReflectCallFuncXXX直接调用的,可是若是要经过反射,那么首先要将方法注册,也就是MethodByName,而后经过反射调动mv.Call
func main() {
	user := User{1, "Allen.Wu", 25}

	// 1. 要经过反射来调用起对应的方法,必需要先经过reflect.ValueOf(interface)来获取到reflect.Value,获得“反射类型对象”后才能作下一步处理
	getValue := reflect.ValueOf(user)

	// 必定要指定参数为正确的方法名
	// 2. 先看看带有参数的调用方法
	methodValue := getValue.MethodByName("ReflectCallFuncHasArgs")
	args := []reflect.Value{reflect.ValueOf("wudebao"), reflect.ValueOf(30)}
	methodValue.Call(args)

	// 必定要指定参数为正确的方法名
	// 3. 再看看无参数的调用方法
	methodValue = getValue.MethodByName("ReflectCallFuncNoArgs")
	args = make([]reflect.Value, 0)
	methodValue.Call(args)
}
复制代码

运行结果:

ReflectCallFuncHasArgs name: wudebao, age: 30 and origal User.Name: Allen.Wu
ReflectCallFuncNoArgs
复制代码

说明

  1. 要经过反射来调用起对应的方法,必需要先经过 reflect.ValueOf(interface) 来获取到 reflect.Value,获得“反射类型对象”后才能作下一步处理

  2. reflect.Value.MethodByName 这 .MethodByName,须要指定准确真实的方法名字,若是错误将直接 panic,MethodByName 返回一个函数值对应的 reflect.Value 方法的名字。

  3. []reflect.Value,这个是最终须要调用的方法的参数,能够没有或者一个或者多个,根据实际参数来定。

  4. reflect.Value 的 Call 这个方法,这个方法将最终调用真实的方法,参数务必保持一致,若是 reflect.Value'Kind 不是一个方法,那么将直接 panic。

  5. 原本能够用 u.ReflectCallFuncXXX 直接调用的,可是若是要经过反射,那么首先要将方法注册,也就是 MethodByName,而后经过反射调用 methodValue.Call

相关文章
相关标签/搜索