Jochen的golang小抄-进阶篇-Reflect

小抄系列进阶篇涉及的概念较多,知识点重要,故每块知识点独立成篇,方便日后笔记的查询

本篇的主题是:反射

反射是指程序在运行时能够“观察”并且修改自己的行为

初窥反射

反射的本质就是在程序运行的时候,动态获取一个对象丰富的类型信息和内存结构,并且通过类型信息可以做出许多灵活的操作,例如获取值,调用方法等待

其实不用反射也可以实现这样的功能,但是需要使用死亡汇编语言直接和内存打交道,这样的话你想要啥信息都可以获取,但由于高级语言需要尽量屏蔽复杂操作,所以只能通过反射去实现

在《Go语言圣经》中反射的定义:

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

反射这玩意,是建立在类型的基础只是的,就是在某一类型不确定的情况下,可以动态的获取某个类型的成员方法的一个机制。这东西在实际开发中遇到就想通了,不必死扣字眼理解~

Go语言反射原理

go语言的反射机制其实是通过接口来实现的,interface是go语言实现抽象的强大工具,当向interface变量赋值一个实体类型时,接口会存储该实体的类型信息

interface变量

一个变量其实包括两部分:类型(type)和值(value),因为go语言是强类型语言,所以变量的类型,和值的类型需要统一!

在go语言中,类型实际上分为两类:静态类型(static type)和具体类型(concrete type

  • 静态类型是变量声明时赋予的类型,也是写代码时能直观看到的类型
    像最常用的intboolstring这种代码上能看到的类型(有时候语法类型推断屏蔽了直观的变量类型,但是通过赋予字面值或其他类型变量你还是能直观的看到变量的静态类型)
  • 具体类型(也叫动态类型)是运行时给变量赋予的实体的类型,是运行时系统才能看到的类型
    例如函数中的接口参数,它可接受多个实现了接口的类型,所以在表面直观上看我们只能看到它是一个xxxinterface的静态类型,但无法具体的知道它表示具体的某一类型(根据传入的实体类型不同而不同),具体的类型只有runtime系统知道

前面我们学过的类型断言的使用,其能否断言成功就是判断他的concrete type是否和断言的类型相同

反射就是为了动态获取一个类型的信息并使用该类型封装的东西
这里提到了动态,我们知道,只有在不确定因素的情况下,我们才会使用动态方式
如果创建一个变量直接指定一个只能接收唯一类型实体的静态类型,那么该变量的实体类型在创建时就已经确定了,它并没有需要“动态”的必要。go语言只有interface类型可以接收多个不同类型的实体,所以只有interface类型才有反射的必要

go语言中,每个interface类型变量都有维护这一个pairpair中记录了变量实际的值和类型(本质就是两个指针,一个指向具体类型,一个指向值)
可以用(Value,Type)(值,类型)表示一组pair

一个类型可能实现了多个接口,那么意味着这个类型的实体可以被多个接口变量接收,不同的接口接收同一个类型实体,它们的pair是一样的

在这里可以进一步的对go语言反射的概念进一步的升华:

  • 反射就是用来检测存储在interface类型变量内部pair信息的一种机制,达到动态获取一个接口变量的值和类型,进而去做一些其他的操作

使用反射

反射对象:Value&Type

前面学习到,反射就是用来检测存储在接口变量内部pair(value和concrete type)的机制

go语言的反射操作定义在reflect包中,其中定义的Value和Type类型是go语言反射操作的核心

go语言reflect包中ValueOfTypeOf是我们常用来访问接口变量内容的两个方法,定义如下:

/ ValueOf returns a new Value initialized to the concrete value
// stored in the interface i. ValueOf(nil) returns the zero Value.
func ValueOf(i interface{}) Value //获取输入参数接口中的数据的值,如果接口为nil返回0


// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type //动态获取输入参数接口中的值的类型,如果为nil返回nil
  • reflect.TypeOf()用来pairType,其得到的是类型的元数据,通过其返回值可以获取丰富的类型元素
  • reflect.ValueOf()用来pairValue,通过返回值可以获取实际存储的值,也可以去改变这个值

使用反射步骤一般如下:

  1. 将变量转换为reflect对象,即上面定义中获取到的ValueType类型,它们里面都包含了大量的方法
  2. 通过reflect对象按需调用其内部定义的方法

而我们学习使用反射,其实就是学习reflect对象提供的方法,其实很多方法都见名知意,不知道作用的去文档瞅一瞅就行了

少说多撸,通过简单示例快速认识下反射的使用

package main

import (
	"fmt"
	"reflect"
)

func main() {
	/*
		反射:通过反射可以动态获取一个接口类型变量的类型和值
	*/
	var i int = 8 //静态类型很直观就能看到是int
	//获取类型
	type1 := reflect.TypeOf(i)
	fmt.Printf("%T\n",type1) //*reflect.rtype
	fmt.Println(type1) //int

	//获取值
	value1 := reflect.ValueOf(i)
	fmt.Printf("%T\n",value1) //reflect.Value
	fmt.Println(value1) //8

	//根据反射类型Value,来获取变量对应的真实类型
	kind := value1.Kind() //获取类型,reflect中定义了go内置类型的枚举值,kind是其中对应的某个值
	fmt.Println(kind) //int
	fmt.Println(kind == reflect.Int) //true

	//获取具体的数值 如果其实际值并非我们使用方法的类型,会报错
	fmt.Printf("%T\n",value1.Int()) //int64
	fmt.Println(value1.Int()) //8

	//所有的类型默认都实现了空接口,所以所有变量类型都可以认为是interface类型的变量,所以可以通过Interface()方法获取实际值
	fmt.Printf("%T\n",value1.Interface()) //int  获取到实际的类型
	fmt.Println(value1.Interface()) //8


}

下面了解更多的反射用法

reflect对象的那些骚操作

动态获取真实值

上面牛刀小试的代码中有看到Value类型的interface()方法可以获取到接口变量的真实值
获取到真实值后,若值为一个结构体,那么通常我们都会想要去获取结构体的成员字段或想要执行一些结构体提供的方法,这里就有两种情况

  • 一种是我们明确这个值的类型,此时我们可以直接通过接口断言进行类型转换,然后直接使用真实值提供的操作了,但这种情况使用场景是比较少的
  • 另一种是我们更常遇到的情况(因为使用反射就是为了动态):不明确值类型,此时我们就无法通过简单的断言操作去将一个类型转换成原有类型了,只能通过Type和Value类型去获取类型和值信息然后通过一些条件判断来得到去动态的进行我们想要的操作

还是少说多撸,下面示例中提供了动态获取结构体类型成员字段和方法的信息的操作

package main

import (
   "fmt"
   "reflect"
)

func main() {
   /*
   	使用反射将一个值转换成原有的类型
   */

   //1.明确知道变量的类型情况下,通过反射将值转变为原有类型
   var n int = 8
   fmt.Printf("n-> type:%T, value:%v\n", n, n) //n-> type:int, value:8
   v1 := reflect.ValueOf(n)                    //转换为反射类型Value
   //通过断言强制将反射类型Value转换为原有类型,可以理解诶为强制转换
   convertValue := v1.Interface().(int)
   fmt.Printf("convertValue-> type:%T, value:%v\n", convertValue, convertValue) //convertValue-> type:int, value:8
   //go语言是强类型语言,转换类型必须完全符合,如果断言的类型与真是类型不符合,则会panic
   //test1 := v1.Interface().(bool) //panic: interface conversion: interface {} is int, not bool
   //fmt.Println(test1)
   //这里有个注意点,就是指针类型和指针的类型不同
   pointerValue := reflect.ValueOf(&n)
   //convertPointer := pointerValue.Interface().(int) //panic: interface conversion: interface {} is *int, not int
   convertPointer := pointerValue.Interface().(*int) //这样才ok
   fmt.Println(convertPointer)                       //0xc00001c0c8

   //2.不知道变量的类型情况下,就没法将直接转换为原类型做方便操作了,只能通过反射提供的机制做动态操作
   p1 := Person{"Jochen", 18}
   GetMessage(p1)
   /*
   	输出:
   		获取的类型是: Person
   		类型的种类是: struct
   		获取变量的数值: {Jochen 18}
   */
}

//定义个可以接收多个类型的方法,动态获取他们的信息
func GetMessage(input interface{}) { //不知道传入的是什么类型
   //通过反射获取传入值的类型
   getType := reflect.TypeOf(input)
   fmt.Println("获取的类型是:", getType.Name())
   fmt.Println("类型的种类是:", getType.Kind())

   //动态获取变量数值
   getValue := reflect.ValueOf(input) //得到的是Value类型
   fmt.Println("获取变量的数值:", getValue)
   //打印虽能看到值,但是若传入的是一个struct,我们想调用其中的方法或具体成员字段该怎么做?
   //获取结构体的字段信息
   //step1:先获取Type对象判断其是否是结构体
   if getType.Kind() == reflect.Struct {
   	//step2:通过Type类型的NumField()方法获取字段数
   	fileNum := getType.NumField()

   	for i := 0; i < fileNum; i++ {
   		//step3:通过Type类型的Field(index)方法传入索引获取File的类型对象,里面提供了获取字段信息的方法
   		fileType := getType.Field(i) //非结构体类型对象使用该方法会panic

   		//step4:通过Value类型的Field(index)方法传入索引获取File的值对象,再通过File对象的interface()方法获取对应的数值
   		fileValue := getValue.Field(i)
   		value := fileValue.Interface()
   		fmt.Println("字段的名称:", fileType.Name, "字段的类型:", fileType.Type, "字段的值为:", value)
   	}

   	//获取结构体的方法信息
   	//step1:通过Type类型的NumMethod()方法获取方法数
   	methodNum := getType.NumMethod()

   	for i := 0; i < methodNum; i++ {
   		//step3:通过Type类型的Method(index)方法传入索引获取Method对象,里面提供了获取方法信息的方法
   		method := getType.Method(i)
   		fmt.Printf("方法名:%s,方法类型%v\n",method.Name,method.Type)

   	}
   }

}

type Person struct {
   Name string
   Age  int
}

func (p Person) Say(Content string) {
   fmt.Println("叽里呱啦:", Content)
}

func (p Person) Run() {
   fmt.Printf("年龄%v的%v,狂奔", p.Name, p.Age)
}

示例中有用到Type对象的Name字段和Kind字段(和Value对象的Type字段和Kind字段一样),它们之间要做好区分:

  • Kind包括slice、map、poniter、struct、interface、string、Array、Function、int、bool等go语言提供的内置基本类型组成
  • Type.Name是静态类型的名称,也就是我们能直观看到的类型名。
    如定义一个新的type jochenInt int,那个该类型的Name就是静态类型jochenInt,而其Kind为int
    如定义一个结构体 type Person sturct{},其静态类型就是Person,Kind为struct,也就是说Kind返回的是基类型,Name返回的是静态类型

动态设置变量的值

通过reflect.ValueOf(v)可以获取到反射对象Value
利用Value对象可以修改真实变量的值,但是该操作,只能建立在当v是指针的前提下。其原理就是通过操作v的地址去修改v的数值(引用类型的变量传递的也是地址,所以引用类型变量是可以直接通过反射提供相对应的方法修改值的,详细操作可以多参考官方api文档,示例主要介绍的是值类型变量的值修改操作)

怎么操作?直接通过代码了解吧

package main

import (
	"fmt"
	"reflect"
)

func main() {
	/*
		通过反射改变变量的值
	*/
	var n int = 8
	fmt.Println("n的值为:", n) //8

	//通过反射改变彼岸来的值(需要操作变量的指针)
	//step1:通过reflect.ValueOf()获取变量的Value对象
	pointer := reflect.ValueOf(&n) //参数必须是指针才能修改值,因为如果直接传变量,传递的值,而不是操作变量了

	//step2:通过Value对象的Elem()方法获取指针所指向的值的Value对象,如果Value不是指针或interface类型会panic
	newValue := pointer.Elem()                  //
	fmt.Println("类型:", newValue.Type())         //int
	fmt.Println("是否可以修改数据:", newValue.CanSet()) //true

	//step3:通过Value对象的SetType方法(Type为指针的类型)
	newValue.SetInt(888)
	//若类型不一致,会panic
	//newValue.SetFloat(3.14) //panic: reflect: call of reflect.Value.SetFloat on int Value
	fmt.Println(n) //888

	//如果reflect.ValueOf的参数不是指针会发生什么?
	v1 := reflect.ValueOf(n)
	//直接尝试赋值,报错的意思就是值对象必须是一个可地址的值
	//v1.SetInt(999) //panic: reflect: reflect.Value.SetInt using unaddressable value
	fmt.Println(v1.CanSet()) //false
	//尝试获取指针类型的Value对象,报错的意思是当前调用Elem方法在int类型的Value对象上(只能在指针类型的Value对象使用)
	//pointerValue := v1.Elem() //panic: reflect: call of reflect.Value.Elem on int Value
	//fmt.Println(pointerValue)

	//要注意,前面我们学过函数的参数传递,知道引用类型传递的是指针,那么是否意味者ValueOf()接收一个引用类型变量就可以直接修改值呢?
	m1 := map[string]string{"123": "456"}
	fmt.Println(m1) //map[123:456]
	v2 := reflect.ValueOf(m1)
	fmt.Println(v2.Kind(), v2.Type()) //map map[string]string
	fmt.Println(v2.CanSet())          //false
	//fmt.Println(v2.Elem()) //panic: reflect: call of reflect.Value.Elem on map Value
	//通过之前的方式看起来引用类型修改值会报错,但与反射对于引用类型专门提供了别的修改方法来修改引用类型的值
	v2.SetMapIndex(reflect.ValueOf("123"), reflect.ValueOf("101112"))
	fmt.Println(m1) //map[123:101112]

	//修改切片的值
	s := []int{1,2,3}
	fmt.Println(s) //[1 2 3]
	v3 := reflect.ValueOf(s)
	fmt.Println(v3.CanSet()) //false
	newValue = v3.Index(0)
	fmt.Println(newValue.CanSet()) //true
	newValue.SetInt(888)
	fmt.Println(s) //[888 2 3]

}

说明:

  • 通过pointer.Elem()获取的是所指向的Value,即获取原始值对应的反射对象(我们需要修改的是这个原始对象,指针对象是为了让反射能找到它),所以调用该方法的必须是一个指针类型的Value
  • 如果我们获取的不是一个指针类型的Value
    • 通过Elem获取原始值对应的对象会panic
    • 通过CanSet方法查询是否可设置值返回的false

如果是复杂的结构体类型,我们该怎么修改其内部的值呢?

package main

import (
	"fmt"
	"reflect"
)

func main() {
	/*
		修改结构体对象字段的值
	*/
	p := Person{"Jochen",18}
	fmt.Println("p修改前值:",p) //p修改前值: {Jochen 18}
	//通过反射,更改对象的值,前提是数据可以被更改
	fmt.Printf("%T\n",p) //main.Person

	v := reflect.ValueOf(&p) //传入变量地址
	if v.Kind() == reflect.Ptr{{
		newValue := v.Elem()  //获取真实值的Value对象
		fmt.Println(newValue.CanSet()) //true

		//通过字段名获取对应的Field对象
		name := newValue.FieldByName("Name")
		name.SetString("张全蛋")
		age := newValue.FieldByName("Age")
		fmt.Println(age)
		age2 := newValue.FieldByName("age")//如果字段不匹配 返回一个非法的value对象,对该对象做操作会panic
		fmt.Println(age2) //<invalid reflect.Value>
		age.SetInt(26)
	}}

	fmt.Println("p修改后值:",p) //p修改后值: {张全蛋 26}

}

type Person struct {
	Name string
	Age int
}

动态方法调用

通过反射来进行方法或函数的调用是属于比较高级的用法,例如一些框架中允许用户自定义方法,框架可以在某些条件成立情况下自动调用这些方法(中间件,过滤器等),因为这些方法对于框架来说都是未知的,所以通过反射动态调用的方式来实现这样的需求就十分合适

通过反射机制进行方法调用使用的是Call()方法,其定义如下:

func (v Value) Call(in []Value) []Value 
//in 函数调用的参数

官方解释:Call方法使用输入的参数in调用v持有的函数。例如,如果len(in) == 3,v.Call(in)代表调用v(in[0], in[1], in[2])(其中Value值表示其持有值)。如果v的Kind不是Func会panic。它返回函数所有输出结果的Value封装的切片。和go代码一样,每一个输入实参的持有值都必须可以直接赋值给函数对应输入参数的类型。如果v持有值是可变参数函数,Call方法会自行创建一个代表可变参数的切片,将对应可变参数的值都拷贝到里面

少说多撸,先看下方法的调用

package main

import (
	"fmt"
	"reflect"
)

func main() {
	/*
		通过反射来进行方法的调用
		思路:
			step1:首先要有接口变量->获取变量的反射对象Value
			step2:获取对应的方法对象,通过MethodByName()方法可以通过方法名获取方法的反射对象Value
			step3:将方法对象进行调用
	*/
	//接口变量(所有变量都实现了空接口,所以所有类型的变量都可以认为是接口变量,因为使用反射方法获取反射对象是把值给的是接口参数)
	p1 := Person{"Jochen", 18}

	//获取反射对象Value
	value := reflect.ValueOf(p1)
	fmt.Printf("Type:%s, Kind:%s\n", value.Type(), value.Kind()) //Type:main.Person, Kind:struct

	//获取方法的反射对象Value(前面学过使用Method方法通过下标获取方法对象,这里换个方式获取)
	sayMethodValue := value.MethodByName("Say")                       //通过方法名获取方法的放射对象Value
	runMethodValue := value.MethodByName("Run")                       //通过方法名获取方法的放射对象Value
	fmt.Printf("类型:%v,种类:%v\n", sayMethodValue.Type(), sayMethodValue.Kind()) //类型:func(string),种类:func
	fmt.Printf("类型:%v,种类:%v\n", runMethodValue.Type(), runMethodValue.Kind()) //类型:func(),种类:func

	//将方法对象通过Call()方法进行调用
	//没有参数的方法Call方法传入nil或空切片即可
	runMethodValue.Call(nil) //没有参数直接传nil
	args1 := make([]reflect.Value,0)
	runMethodValue.Call(args1) //没有参数传空切片

	//如果有参数需要构建参数列表
	args2 := []reflect.Value{reflect.ValueOf("我反着调,怎么说?")}
	//如果参数类型或个数不匹配会panic
	//args2 = append(args2, reflect.ValueOf("第二个参数")) //reflect.Value.call(0x4e6240, 0xc0000ae020, 0x693, 0x4f43e8, 0x4, 0xc0000982d0, 0x2, 0x2, 0x2, 0x18, ...)
	sayMethodValue.Call(args2)
	
}

type Person struct {
	Name string
	Age  int
}

func (p Person) Say(Content string) {
	fmt.Println("叽里呱啦:", Content)
}

func (p Person) Run() {
	fmt.Printf("年龄%v的%v,狂奔", p.Name, p.Age)
}

通过反射,也可以调用函数

回顾下函数,函数其实和普通变量一样,其可以认为是一种变量类型,而且是引用类型的变量

函数可以认为是一种变量类型,通过Value()获取函数的反射对象Value,然后判断它的Kind是否为func,若是则可以执行Call()方法进行函数的调用,十分easy

package main

import (
	"fmt"
	"reflect"
	"strconv"
)

func main() {
	/*
		通过反射进行函数调用
	*/
	//函数可以看做是接口变量类型
	//step1:获取函数反射对象Value
	value1 := reflect.ValueOf(f1)
	fmt.Printf("type:%s, kind:%s\n", value1.Type(), value1.Kind()) //type:func(), kind:func
	value2 := reflect.ValueOf(f2)
	fmt.Printf("type:%s, kind:%s\n", value2.Type(), value2.Kind()) //ctype:func(int, string) string, kind:func

	//step2:判断kind是否为func类型,这一步因为示例用的都是函数类型,所以没有意义,不判断了

	//step3:Call方法进行函数调用
	value1.Call(nil)

	arg := []reflect.Value{reflect.ValueOf(888),reflect.ValueOf("hello")}
	res := value2.Call(arg)
	fmt.Printf("Call返回值类型%T, 值%v\n",res,res) //Call返回值类型[]reflect.Value, 值[hello888]
	//可以看到Call方法的返回值是一个Value类型的切片,当没有返回值时,则是一个空切片。多个返回值则切片对应多个元素
	for _, v := range res {
		fmt.Printf("返回值类型:%s, 值:%v\n",v.Type(),v.Interface()) //返回值类型:string, 值:hello888
	}

}

func f1() {
	fmt.Println("没有带参数的函数")
}

func f2(i int, s string) string {
	fmt.Printf("带有参数类型为%T, %T的函数,并且返回类型%T的返回值\n", i, s, s)
	return s + strconv.Itoa(i)
}

over~


本系列学习资料参考:
https://www.bilibili.com/video/BV1jJ411c7s3?p=15
https://books.studygolang.com/The-Golang-Standard-Library-by-Example/chapter01/01.1.html

posted @ 2021-03-27 16:56  .Jochen  阅读(246)  评论(0编辑  收藏  举报