golang的反射与实践(下)

上篇说了下反射该怎么用,如今咱们来看一看使用反射的实际状况,深刻理解一下golang

这篇由于是实践篇,因此有大量的代码示例来进行演示,由于只是演示反射的使用,因此对一些复杂的错误机制没作处理json

反射自己并不难,看懂了上一章反射究竟是干吗用的,何时用,这一章其实很是好懂设计模式

说到底就是将reflect包提供给咱们的方法,进行一些组合使用罢了,说土一点就是调用下API数组

没看上篇的能够先看看golang的反射与实践(上)微信

反射的实践操做

好了,我们开始进行实践app

先把咱们的准备工做作好,先定义一个Struct,再给这个结构体加上一些方法函数

// Employee 员工
type Employee struct {
	Name string `json:"emp_name"`
	Age  int    `json:"emp_age"`
	Sex  int
}

// GetSum 返回两数之和
func (e *Employee) GetSum(n1, n2 int) int {
	return n1 + n2
}

// Set 接受值,给结构体e赋值
func (e *Employee) Set(name string, age, sex int) {
	e.Name = name
	e.Age = age
	e.Sex = sex
}

// Print 打印结构体*Employee 
func (e *Employee) Print() {
	log.Print("======Start======")
	log.Print(e)
	log.Print("======End======")
}
复制代码

随便给这个结构体写了几个方法,咱们主要是看,咱们如何使用反射在运行时对变量进行一个操做性能

使用反射来遍历结构体的字段值,并获取结构体的tag标签

先来看个常规用法学习

// GetStruct 获取结构体的字段及tag
func GetStruct(i interface{}) {
	rType := reflect.TypeOf(i)
	rVal := reflect.ValueOf(i)

	kd := rVal.Kind()

	// 若是是传进来的是指针类型
	// 则获取指针值
	if kd == reflect.Ptr {
		rType = rType.Elem()
		rVal = rVal.Elem()
		kd = rVal.Kind()
	}

	if kd != reflect.Struct {
		log.Panicf("Kind is %v not struct ", kd)
	}
	// 获取结构体的字段数
	sNum := rVal.NumField()
	log.Printf("Struct has %v fields ", sNum)
	// 遍历结构体的全部字段
	for i := 0; i < sNum; i++ {
		log.Printf("Field %d value is %v", i, rVal.Field(i))
		// 获取Struct的tag,使用Type类型获取
		tag := rType.Field(i).Tag.Get("json")
		if tag == "" {
			log.Printf("Field %d hasn't tag %v ", i, tag)
			continue
		}
		log.Printf("Field %d tag is %v ", i, tag)
	}
}
复制代码

咱们定义一个方法GetStruct(i interface{}),由于入参是interface{}类型,因此这个方法能够接收并处理全部的数据类型。这就是反射的牛逼之处了测试

遗憾的是,反射的性能比较低。后面我们对性能进行分析时再拿出来聊聊

测试用例以下

func TestGetStruct(t *testing.T) {
	emp := &Employee{}
	emp.Set("闹闹", 99, 0)
	GetStruct(emp)
}
复制代码

执行结果以下图所示

这个函数接受的参数是interface,也就是说,经过这个函数,无论入参传递了什么样的结构体,咱们能够知道这个结构体有什么标签,有几个方法

获取tag标签的用处就是对咱们的结构体进行序列化时使用,将结构体的字段名变成咱们须要的别名

想深刻了解的童鞋,能够参考下encoding/json包的使用方式

获取并调用结构体的方法

// CallMethod 调用结构体方法
// i : 传入的struct
// methodByName : 调用结构体的方法名
func CallMethod(i interface{}, methodByName string) {
	rVal := reflect.ValueOf(i)
	rType := reflect.TypeOf(i)
	log.Printf("Type is %v Kind is %v", rType, rType.Kind())

	// 获取结构体有多少个方法
	numOfMethod := rVal.NumMethod()
	log.Printf("Struct has %d method", numOfMethod)
	// 声明Value数组
	var params []reflect.Value
	// 声明一个Value类型,用于接收方法
	var method reflect.Value

	if methodByName == "GetSum" {
		// 调用方法时的参数
		params = append(params, reflect.ValueOf(10))
		params = append(params, reflect.ValueOf(88))

	}
	if methodByName == "Set" {
		// 调用方法时的参数
		params = append(params, reflect.ValueOf("闹闹吃鱼"))
		params = append(params, reflect.ValueOf(18))
		params = append(params, reflect.ValueOf(0))
	}
	// 获取方法
	method = rVal.MethodByName(methodByName)
	if !method.IsValid() {
		// 若是结构体不存在此方法,输出Panic
		log.Panic("Method is invalid")
	}
	result := method.Call(params)
	if len(result) > 0 {
		// 若是函数存在返回值,则打印第一条
		log.Println("Call result is ", result[0])
	}
}
复制代码

这里值得注意一点的就是,咱们经过反射的Call去调用函数,传入的参数的类型是reflect.Value类型,并非咱们定义函数时的int类型

因此在调用函数时传入的参数须要进行一个类型转换

给大家附上测试用例,大家能够本身调试跑跑,会发现,无论你传的结构体的字段是什么,我都进行统一处理了

func TestCallMethod(t *testing.T) {
	emp := &Employee{}
	emp.Set("闹闹", 99, 0)
	emp.Print()
	CallMethod(emp, "Set")
	emp.Print()
}
复制代码

修改字段值

// ModifyField 修改字段值
func ModifyField(i interface{}, filedName string) {
	rVal := reflect.ValueOf(i)
	filed := rVal.Elem().FieldByName(filedName)
	if !filed.IsValid() {
		log.Panic("filedName is invalid")
	}
	filed.SetString("闹闹")
}
复制代码

运行时修改结构体的字段,主要就是作到一个通用性,好比上述的例子,不论是什么结构体

依然附上测试用例

func TestModifyField(t *testing.T) {
	emp := &Employee{}
	ModifyField(emp, "Name")
}
复制代码

无论传入的结构体是什么,只要包含了filedName(咱们指定的字段名),咱们就能够对其进行值的更改

假如咱们有100个结构体,须要对name字段进行修改,经过反射的机制,咱们代码的耦合度将大大的下降

定义适配器,用做统一处理接口

// Bridge 适配器
// 能够实现调用任意函数
func Bridge(call interface{}, args ...interface{}) {
	var (
		function reflect.Value
		inValue  []reflect.Value
	)
	n := len(args)
	// 将参数转换为Value类型
	inValue = make([]reflect.Value, n)
	for i := 0; i < n; i++ {
		inValue[i] = reflect.ValueOf(args[i])
	}
	// 得到函数的Value类型
	function = reflect.ValueOf(call)
	// 传参,调用函数
	function.Call(inValue)
}
复制代码

写了个测试用例,函数是咱们在调用Bridge前就已经定义好了

func TestBridge(t *testing.T) {
	call1 := func(v1, v2 int) {
		log.Println(v1, v2)
	}
	call2 := func(v1, v2 int, str string) {
		log.Println(v1, v2, str)
	}

	Bridge(call1, 1, 2)
	Bridge(call2, 2, 3, "callTest")
}

复制代码

两个函数是不一样的函数,可是均可以经过Bridge进行执行

适配器有什么用呢?若是不知道的童鞋,能够去看看设计模式「适配器模式」

由于本篇幅只是说如何在实战中应用反射,因此这里就不讲解设计模式了

使用反射建立,并操做结构体

// CreateStruct 使用反射建立结构体
// 并给结构体赋值
func CreateStruct(i interface{}) *Employee {
	var (
		structType  reflect.Type
		structValue reflect.Value
	)
	// 获取传入结构体指向的Type类型
	structType = reflect.TypeOf(i).Elem()
	// 建立一个结构体
	// structValue持有一个指向类型为Type的新申请的指针
	structValue = reflect.New(structType)
	// 转换成咱们要建立的结构体
	modle := structValue.Interface().(*Employee)
	// 取得structValue指向的值
	structValue = structValue.Elem()
	// 给结构体赋值
	structValue.FieldByName("Name").SetString("闹闹吃鱼")
	structValue.FieldByName("Age").SetInt(100)
	return modle
}
复制代码

使用方式就看看测试用例

func TestCreateStruct(t *testing.T) {
	emp := &Employee{
		Name: "NaoNao",
		Age:  18,
		Sex:  1,
	}
	emp.Print()// &{NaoNao 18 1}
	newEmp := CreateStruct(emp)
	newEmp.Print()// &{闹闹吃鱼 100 0}
}
复制代码

可能你会问,CreateStruct的入参不是interface{}吗?可为何我传一个任意的结构体,却要给返回一个指定的结构体呢?就不能我传什么结构体进去,就返回什么结构体出来吗?

理想老是丰满的,现实倒是很是骨感,虽然咱们反射的方法实现,都是将入参写为interface{},但使用反射并非意味着咱们必定就写了一个万能的程序

还记得上一篇提到的,变量reflect.Value之间该如何转换吗?

我们再复习一下:变量<------>interface{}<------>reflect.Value

咱们不论是把Value转为结构体,仍是转为基本类型,咱们都须要在编译前肯定转换后的类型

换句话说,只要咱们在运行时牵扯到类型的转换,咱们都须要各类if来判断是否能转换成咱们须要的类型

本文以大量的代码实现来阐述反射该怎么用,说实话,挺无聊的

写这篇文章的目的就是让你拿电脑上去编译跑跑,或者何时想到要用反射了,能够拿出来瞅瞅,看看什么地方须要用到反射,反射又能够干什么

image

微信扫码关注公众号「闹闹吃鱼」,还可领取Go语言学习大礼包,入门到进阶再也不了无头绪

相关文章
相关标签/搜索