站长网 语言 用手写一个工具的过程讲清楚Go反射的使用方式和应用场景

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

今天来聊一个平时用的不多,但是很多框架或者基础库会用到的语言特性–反射,反射并不是Go语言独有的能力,其他编程语言都有。这篇文章的目标是简单地给大家梳理一下反射的应用场景和使用方法。我们平时写代码能接触到与反射联系比较紧密的一个东西是结构体

今天来聊一个平时用的不多,但是很多框架或者基础库会用到的语言特性–反射,反射并不是Go语言独有的能力,其他编程语言都有。这篇文章的目标是简单地给大家梳理一下反射的应用场景和使用方法。

 

我们平时写代码能接触到与反射联系比较紧密的一个东西是结构体字段的标签,这个我准备放在后面的文章再梳理。

 

我准备通过用反射搞一个通用的SQL构造器的例子,带大家掌握反射这个知识点。这个是看了国外一个博主写的例子,觉得思路很好,我又对其进行了改进,让构造器的实现更丰富了些。

 

本文的思路参考自:https://golangbot.com/reflection/ ,本文内容并非只是对原文的简单翻译,具体看下面的内容吧~!

 

什么是反射

反射是程序在运行时检查其变量和值并找到它们类型的能力。听起来比较笼统,接下来我通过文章的例子一步步带你认识反射。

 

为什么需要反射

当学习反射的时候,每个人首先会想到的问题都是 “为什么我们要在运行时检查变量的类型呢,程序里的变量在定义的时候我们不都已经给他们指定好类型了吗?” 确实是这样的,但也并非总是如此,看到这你可能心里会想,大哥,你在说什么呢,em… 还是先写一个简单的程序,解释一下。

 

package main 

 

import (   

    "fmt" 

 

func main() {   

    i := 10 

    fmt.Printf("%d %T", i, i) 

在上面的程序里, 变量i的类型在编译时是已知的,我们在下一行打印了它的值和类型。

 

现在让我们理解一下 ”在运行时知道变量的类型的必要“。假设我们要编写一个简单的函数,它将一个结构体作为参数,并使用这个参数创建一个SQL插入语句。

 

考虑一下下面这个程序:

 

package main 

 

import (   

    "fmt" 

 

type order struct {   

    ordId      int 

    customerId int 

 

func main() {   

    o := order{ 

        ordId:      1234, 

        customerId: 567, 

    } 

    fmt.Println(o) 

我们需要写一个接收上面定义的结构体o作为参数,返回类似INSERT INTO order VALUES(1234, 567)这样的SQL语句。这个函数定义写来很容易,比如像下面这样。

 

package main 

 

import (   

    "fmt" 

 

type order struct {   

    ordId      int 

    customerId int 

 

func createQuery(o order) string {   

    i := fmt.Sprintf("INSERT INTO order VALUES(%d, %d)", o.ordId, o.customerId) 

    return i 

 

func main() {   

    o := order{ 

        ordId:      1234, 

        customerId: 567, 

    } 

    fmt.Println(createQuery(o)) 

上面例子的createQuery使用参数o 的ordId和customerId字段创建SQL。

 

现在让我们将我们的SQL创建函数定义地更抽象些,下面还是用程序附带说明举一个案例,比如我们想泛化我们的SQL创建函数使其适用于任何结构体。

 

package main 

 

type order struct {   

    ordId      int 

    customerId int 

 

type employee struct {   

    name string 

    id int 

    address string 

    salary int 

    country string 

 

func createQuery(q interface{}) string {   

现在我们的目标是,改造createQuery函数,让它能接受任何结构作为参数并基于结构字段创建INSERT 语句。比如如果传给createQuery的参数不再是order类型的结构体,而是employee类型的结构体时

 

e := employee { 

       name: "Naveen", 

       id: 565, 

       address: "Science Park Road, Singapore", 

       salary: 90000, 

       country: "Singapore", 

   } 

那它应该返回的INSERT语句应该是

 

INSERT INTO employee (name, id, address, salary, country)  

VALUES("Naveen", 565, "Science Park Road, Singapore", 90000, "Singapore")   

由于createQuery 函数要适用于任何结构体,因此它需要一个 interface{}类型的参数。为了说明问题,简单起见,我们假定createQuery函数只处理包含string 和 int 类型字段的结构体。

 

编写这个createQuery函数的唯一方法是检查在运行时传递给它的参数的类型,找到它的字段,然后创建SQL。这里就是需要反射发挥用的地方啦。在后续步骤中,我们将学习如何使用Go语言的反射包来实现这一点。

 

Go语言的反射包

Go语言自带的reflect包实现了在运行时进行反射的功能,这个包可以帮助识别一个interface{}类型变量其底层的具体类型和值。我们的createQuery函数接收到一个interface{}类型的实参后,需要根据这个实参的底层类型和值去创建并返回INSERT语句,这正是反射包的作用所在。

 

在开始编写我们的通用SQL生成器函数之前,我们需要先了解一下reflect包中我们会用到的几个类型和方法,接下来我们先逐个学习一下。

 

reflect.Type 和 reflect.Value

 

经过反射后interface{}类型的变量的底层具体类型由reflect.Type表示,底层值由reflect.Value表示。reflect包里有两个函数reflect.TypeOf() 和reflect.ValueOf() 分别能将interface{}类型的变量转换为reflect.Type和reflect.Value。这两种类型是创建我们的SQL生成器函数的基础。

 

让我们写一个简单的例子来理解这两种类型。

 

package main 

 

import (   

    "fmt" 

    "reflect" 

 

type order struct {   

    ordId      int 

    customerId int 

 

func createQuery(q interface{}) {   

    t := reflect.TypeOf(q) 

    v := reflect.ValueOf(q) 

    fmt.Println("Type ", t) 

    fmt.Println("Value ", v) 

 

 

func main() {   

    o := order{ 

        ordId:      456, 

        customerId: 56, 

    } 

    createQuery(o) 

 

上面的程序会输出:

 

Type  main.order   

Value  {456 56}  

上面的程序里createQuery函数接收一个interface{}类型的实参,然后把实参传给了reflect.Typeof和reflect.Valueof 函数的调用。从输出,我们可以看到程序输出了interface{}类型实参对应的底层具体类型和值。

 

Go语言反射的三法则

 

这里插播一下反射的三法则,他们是:

 

从接口值可以反射出反射对象。

从反射对象可反射出接口值。

要修改反射对象,其值必须可设置。

反射的第一条法则是,我们能够吧Go中的接口类型变量转换成反射对象,上面提到的reflect.TypeOf和 reflect.ValueOf 就是完成的这种转换。第二条指的是我们能把反射类型的变量再转换回到接口类型,最后一条则是与反射值是否可以被更改有关。三法则详细的说明可以去看看德莱文大神写的文章 Go反射的实现原理,文章开头就有对三法则说明的图解,再次膜拜。

 

下面我们接着继续了解完成我们的SQL生成器需要的反射知识。

 

reflect.Kind

 

reflect包中还有一个非常重要的类型,reflect.Kind。

 

reflect.Kind和reflect.Type类型可能看起来很相似,从命名上也是,Kind和Type在英文的一些Phrase是可以互转使用的,不过在反射这块它们有挺大区别,从下面的程序中可以清楚地看到。

 

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   

通过输出让我们清楚了两者之间的区别。reflect.Type 表示接口的实际类型,即本例中main.order 而Kind表示类型的所属的种类,即main.order是一个「struct」类型,类似的类型map[string]string的Kind就该是「map」。

 

反射获取结构体字段的方法

 

我们可以通过reflect.StructField类型的方法来获取结构体下字段的类型属性。reflect.StructField可以通过reflect.Type提供的下面两种方式拿到。

 

// 获取一个结构体内的字段数量 

NumField() int 

// 根据 index 获取结构体内字段的类型对象 

Field(i int) StructField 

// 根据字段名获取结构体内字段的类型对象 

FieldByName(name string) (StructField, bool) 

reflect.structField是一个struct类型,通过它我们又能在反射里知道字段的基本类型、Tag、是否已导出等属性。

 

type StructField struct { 

 Name string 

 Type      Type      // field type 

 Tag       StructTag // field tag string 

  …… 

与reflect.Type提供的获取Field信息的方法相对应,reflect.Value也提供了获取Field值的方法。

 

func (v Value) Field(i int) Value { 

… 

 

func (v Value) FieldByName(name string) Value { 

… 

这块需要注意,不然容易迷惑。下面我们尝试一下通过反射拿到order结构体类型的字段名和值

本文来自网络,不代表站长网立场,转载请注明出处:https://www.tzzz.com.cn/html/biancheng/yuyan/2021/1103/19136.html

作者: dawei

【声明】:站长网内容转载自互联网,其相关言论仅代表作者个人观点绝非权威,不代表本站立场。如您发现内容存在版权问题,请提交相关链接至邮箱:bqsm@foxmail.com,我们将及时予以处理。
联系我们

联系我们

0577-28828765

在线咨询: QQ交谈

邮箱: xwei067@foxmail.com

工作时间:周一至周五,9:00-17:30,节假日休息

返回顶部