43

反射 - Go 语言学习笔记

 4 years ago
source link: https://www.tuicool.com/articles/qAvaQbF
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

什么是 Go 的反射

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

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

为什么需要反射

需要反射的 2 个常见场景:

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

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

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

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

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 "???"
    }
}
复制代码

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

没有办法来检查未知类型的表示方式,被卡住了。这就是为何需要反射的原因。

反射是如何实现的

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK