用手写一个工具的过程讲清楚 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结构体类型的字段名和值

package main import (  "fmt"  "reflect" ) type order struct {   ordId      int  customerId int } func createQuery(q interface{ }) {   t := reflect.TypeOf(q)  if t.Kind() != reflect.Struct {    panic("unsupported argument type!")  }  v := reflect.ValueOf(q)  for i:=0; i < t.NumField(); i++ {    fmt.Println("FieldName:", t.Field(i).Name, "FiledType:", t.Field(i).Type,    "FiledValue:", v.Field(i))  } } func main() {   o := order{    ordId:      456,   customerId: 56,  }  createQuery(o) } 

上面的程序会输出:

FieldName: ordId FiledType: int FiledValue: 456 FieldName: customerId FiledType: int FiledValue: 56 

除了获取结构体字段名称和值之外,还能获取结构体字段的Tag,这个放在后面的文章我再总结吧,不然篇幅就太长了。

reflect.Value转换成实际值

现在离完成我们的SQL生成器还差最后一步,即还需要把reflect.Value转换成实际类型的值,reflect.Value实现了一系列Int(), String(),Float()这样的方法来完成其到实际类型值的转换。

用反射搞一个SQL生成器

上面我们已经了解完写这个SQL生成器函数前所有的必备知识点啦,接下来就把他们串起来,加工完成createQuery函数。

这个SQL生成器完整的实现和测试代码如下:

package main import (  "fmt"  "reflect" ) 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 {   t := reflect.TypeOf(q)  v := reflect.ValueOf(q)  if v.Kind() != reflect.Struct {    panic("unsupported argument type!")  }  tableName := t.Name() // 通过结构体类型提取出SQL的表名  sql := fmt.Sprintf("INSERT INTO %s ", tableName)  columns := "("  values := "VALUES ("  for i := 0; i < v.NumField(); i++ {    // 注意reflect.Value 也实现了NumField,Kind这些方法   // 这里的v.Field(i).Kind()等价于t.Field(i).Type.Kind()   switch v.Field(i).Kind() {    case reflect.Int:    if i == 0 {      columns += fmt.Sprintf("%s", t.Field(i).Name)     values += fmt.Sprintf("%d", v.Field(i).Int())    } else {      columns += fmt.Sprintf(", %s", t.Field(i).Name)     values += fmt.Sprintf(", %d", v.Field(i).Int())    }   case reflect.String:    if i == 0 {      columns += fmt.Sprintf("%s", t.Field(i).Name)     values += fmt.Sprintf("%s", v.Field(i).String())    } else {      columns += fmt.Sprintf(", %s", t.Field(i).Name)     values += fmt.Sprintf(", %s", v.Field(i).String())    }   }  }  columns += "); "  values += "); "  sql += columns + values  fmt.Println(sql)  return sql } func main() {   o := order{    ordId:      456,   customerId: 56,  }  createQuery(o)  e := employee{    name:    "Naveen",   id:      565,   address: "Coimbatore",   salary:  90000,   country: "India",  }  createQuery(e) } 

同学们可以把代码拿到本地运行一下,上面的例子会根据传递给函数不同的结构体实参,输出对应的标准SQL插入语句

INSERT INTO order (ordId, customerId); VALUES (456, 56);  INSERT INTO employee (name, id, address, salary, country); VALUES (Naveen, 565, Coimbatore, 90000, India);  

总结

这篇文章通过利用反射完成一个实际应用来教会大家Go语言反射的基本使用方法,虽然反射看起来挺强大,但使用反射编写清晰且可维护的代码非常困难,应尽可能避免,仅在绝对必要时才使用。 

我的看法是如果是要写业务代码,根本不需要使用反射,如果要写类似encoding/json,gorm这些样的库倒是可以利用反射的强大功能简化库使用者的编码难度。

IT科技
上一篇:域后缀首选.com,.net,然后是.cn。后缀选择不当,导致流量损失。域名是企业与互联网网址之间的链接,关键是企业在网络上存在的标志。因此,选择好域名是开展网上工作的首要重要条件。
下一篇:域名资源有限,好域名更是有限,但机会随时都有,这取决于我们能否抓住机会。一般观点认为,国内域名注册太深,建议优先考虑外国注册人。外国注册人相对诚实,但价格差别很大,从几美元到几十美元不等。域名投资者应抓住机遇,尽早注册国外域名。