当前位置: 首页 > 科技观察

用手写一个工具的过程讲清楚 Go反射的使用方法和应用场景

时间:2023-03-18 22:53:18 科技观察

亲手写一个工具的过程,把Go反射的用法和应用场景解释的很清楚,其他编程语言所具有的独特能力。本文的目的是简单梳理一下反射的应用场景和使用方法。我们平时写代码的时候和反射密切相关的一个东西就是structure字段的标号,这个我打算在后面的文章中进行梳理。下面我将通过使用反射做一个通用的SQL构造函数的例子,带大家掌握反射的知识点。这是一个国外博主写的例子。我觉得这个想法很好,我对其进行了改进,让构造函数的实现更加丰富。本文思路参考:https://golangbot.com/reflection/,本文内容并非只是对原文的简单翻译,详情请看下文内容~!什么是反射反射是一个程序,它在运行时检查它的变量和值,并能够找到它们的类型。听起来比较笼统,接下来我会通过文中的例子,带大家一步步理解反射。为什么需要反思?学习反射时,大家第一个想到的问题就是“为什么要在运行时检查变量的类型?我们在程序中定义变量的时候不是已经指定了变量的类型吗?”?的确如此,但情况并非总是如此。看到这里,你可能会想,兄弟,你在说什么,em...先写个简单的程序,说明一下。packagemainimport("fmt")funcmain(){i:=10fmt.Printf("%d%T",i,i)}在上面的程序中,变量i的类型在编译时已知,我们在下一行打印它的值和类型。现在让我们了解“在运行时知道变量类型的必要性”。假设我们要编写一个简单的函数,它以一个结构体作为参数,并使用这个参数来创建一个SQL插入语句。考虑以下程序:packagemainimport("fmt")typeorderstruct{ordIdintcustomerIdint}funcmain(){o:=order{ordId:1234,customerId:567,}fmt.Println(o)}我们需要编写一个接收上述内容的结构将Bodyo定义为参数,返回一条SQL语句,如INSERTINTOorderVALUES(1234,567)。这个函数定义很容易写,例如如下。packagemainimport("fmt")typeorderstruct{ordIdintcustomerIdint}funccreateQuery(oorder)string{i:=fmt.Sprintf("INSERTINTOorderVALUES(%d,%d)",o.ordId,o.customerId)return}funcmain(){o:=order{ordId:1234,customerId:567,}fmt.Println(createQuery(o))}上面例子中的createQuery使用参数o的ordId和customerId字段来创建SQL。现在让我们更抽象地定义我们的SQL创建函数。下面用程序来举例。例如,我们希望将我们的SQL创建函数概括为适用于任何结构。packagemaintypeorderstruct{ordIdintcustomerIdint}typeemployeestruct{namestringidintaddressstringsalaryintcountrystring}funccreateQuery(qinterface{})string{}现在我们的目标是修改createQuery函数以接受任何结构作为参数,并根据结构字段创建INSERT语句。比如传给createQuery的参数不再是订单类型结构,而是员工类型结构e:=employee{name:"Naveen",id:565,address:"ScienceParkRoad,Singapore",salary:90000,country:"Singapore",}那么它应该返回的INSERT语句应该是INSERTINTOemployee(name,id,address,salary,country)VALUES("Naveen",565,"ScienceParkRoad,Singapore",90000,"Singapore")因为createQuery函数应该与任何结构一起工作,因此它需要一个接口类型的参数{}。为了说明问题,为了简单起见,我们假设createQuery函数只处理包含string和int类型字段的结构。编写此createQuery函数的唯一方法是在运行时检查传递给它的参数的类型,找到它的字段,然后创建SQL。这就是反射发挥作用的地方。在后面的步骤中,我们将学习如何使用Go语言的反射包来执行此操作。Go语言的反射包Go语言自带的reflect包实现了运行时反射的功能。此包可以帮助识别interface{}类型变量的底层特定类型和值。我们的createQuery函数接收到一个interface{}类型的实参后,需要根据实参的底层类型和值创建并返回一条INSERT语句,这正是反射包所做的。在我们开始编写我们的通用SQL生成器函数之前,我们需要了解我们将在reflect包中使用的几种类型和方法,然后我们将一一学习。reflect.Type和reflect.Value反射后,typeinterface{}变量的底层具体类型由reflect.Type表示,底层值由reflect.Value表示。reflect包中有两个函数reflect.TypeOf()和reflect.ValueOf(),可以将interface{}类型的变量分别转化为reflect.Type和reflect.Value。这两种类型是创建我们的SQL生成器函数的基础。让我们写一个简单的例子来理解这两种类型。packagemainimport("fmt""reflect")typeorderstruct{ordIdintcustomerIdint}funccreateQuery(qinterface{}){t:=reflect.TypeOf(q)v:=reflect.ValueOf(q)fmt.Println("Type",t)fmt.println("Value",v)}funcmain(){o:=order{ordId:456,customerId:56,}createQuery(o)}上面的程序会输出:Typemain.orderValue{45656}上面的createQuery函数程序接收一个interface{}类型的实参,然后将实参传递给reflect.Typeof和reflect.Valueof函数的调用。从输出中,我们可以看到程序输出了接口{}类型参数对应的底层具体类型和值。Go语言中反射的三大规则这里介绍一下反射的三大规则,它们是:反射对象可以从接口值中反射出来。接口值可以从反射对象中反映出来。要修改反射对象,它的值必须是可设置的。反射的第一个规则就是我们可以把Go中的接口类型变量转换成一个反射对象。上面提到的reflect.TypeOf和reflect.ValueOf就是完成的转换。第二项是指我们可以将反射类型的变量转换回接口类型,最后一项是关于反射值是否可以改变的。三个规则的详细解释可以看大神Delevingne写的关于Go反射实现原理的文章。文章开头有一张图解释这三个规则。让我们继续了解完成我们的SQL生成器所需的反射知识。reflect.Kindreflect包中还有一个很重要的类型,reflect.Kind。reflect.Kind和reflect.Type类型可能看起来相似,在命名方面也是如此。有些英语短语的种类和类型可以互换使用,但它们在反映上有很大的不同。从下面的程序中可以清楚地看出。packagemainimport("fmt""reflect")typeorderstruct{ordIdintcustomerIdint}funccreateQuery(qinterface{}){t:=reflect.TypeOf(q)k:=t.Kind()fmt.Println("Type",t)fmt.Println("Kind",k)}funcmain(){o:=order{ordId:456,customerId:56,}createQuery(o)}上面程序会输出Typemain.orderKindstruct通过输出让我们知道两者的关系区别。reflect.Type表示接口的实际类型,即本例中的main.order,Kind表示类型的类型,即main.order是一个“struct”类型,类似类型映射的Kind[string]string应该是“地图”。通过反射获取结构字段的方法我们可以通过reflect.StructField类型的方法获取结构下字段的类型属性。reflect.StructField可以通过reflect.Type提供的以下两个方法获取。//获取结构体中的字段个数NumField()int//根据索引获取结构体中字段的类型对象Field(iint)StructField//根据索引获取结构体中字段的类型对象fieldnameFieldByName(namestring)(StructField,bool)reflect.structField是一个struct类型,通过它我们可以知道反射中字段的基本类型,Tag,是否导出等属性。typeStructFieldstruct{NamestringTypeType//fieldtypeTagStructTag//fieldtagstring……}对应于reflect.Type提供的获取Field信息的方法,reflect.Value也提供了获取Field值的方法。func(vValue)Field(iint)Value{...}func(vValue)FieldByName(namestring)Value{...}需要注意,否则容易混淆。接下来我们尝试通过反射获取订单结构类型的字段名和值()!=reflect.Struct{panic("unsupportedargumenttype!")}v:=reflect.ValueOf(q)fori:=0;i