美文网首页
Go教程第二十五篇:Go中的反射

Go教程第二十五篇:Go中的反射

作者: 大风过岗 | 来源:发表于2020-05-19 11:18 被阅读0次

Go中的反射

本文是《Go系列教程》的第二十六篇文章。

反射是Go的高级特性之一。我将尽力讲解地简单些。

本教程具有下面几个章节:

  • 什么是反射?

  • 考察一个变量,并且寻找它的类型的需要是什么?

  • 反射包

    . reflect.Type 和 reflect.Value

    . reflect.Kind

    . NumField() 和 Field() methods

    . Int() 和 String() methods

  • 完整的程序

  • 应该使用反射吗?

我们将对这些内容一个个地加以讨论。

什么是反射?

反射指的是一个程序可以在运行时检查变量以及它的值并查找他们的类型。你可能不太理解这是什么意思,不过,没事。在读完本文章之后,你会有一个清晰的理解。来跟着我一探究竟。

检查变量并查找其类型的需要是什么?

许多人在学习反射的时候,都有一个问题,那就是为什么我们需要在运行时期检查变量,查找它的类型呢。我们程序中的每一个变量都是由我们自己定义的。我们在编译期就知道它的类型。确实,在大多数情况下是这样,但并不是所有时候都是如此。

我们写个简单的程序,来解释一下。

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作为参数,并返回下面这个SQL语句。

insert into order values(1234, 567)

这个函数很好写,我们来写一下。

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字段创建了一个插入语句。程序的输出如下:

insert into order values(1234, 567)

此时,我们让这个函数更高级一点。如果我们想让这个函数可以接收任何的结构体,并根据结构体生成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 {
}

func main() {

}

我们的目标是完成createQuery函数,让它可以接收任何的结构体作为参数,并基于结构体的字段生成相应的SQL语句。
例如,我们把下面这个结构体作为参数传递过去。

o := order {
    ordId: 1234,
    customerId: 567
}

那么,我们的createQuery函数就应该返回:

insert into order values (1234, 567)

类似地,如果我们传递了下面这个结构体:

e := employee {
       name: "Naveen",
       id: 565,
       address: "Science Park Road, Singapore",
       salary: 90000,
       country: "Singapore",
   }

它应该返回:

insert into employee values("Naveen", 565, "Science Park Road, Singapore", 90000, "Singapore")

既然createQuery函数应该接受任意的结构体参数的话,那也就意味着,它接收一个接口作为参数。简单起见,我们只处理含有string和int类型的字段。

createQuery可以处理所有的结构体,那么编写此函数的唯一的方式就是在运行时检查此结构体的类型,并找到它都包含哪些字段,据此创建SQL语句。这时候,反射就十分有用了。
下一步,我们就要学习下如何使用反射包来实现这个需求。

反射包

Go的反射包实现了运行时反射。反射包可用于识别底层的具体数据类型,以及一个接口变量的值。而这正是我们所需要的。createQuery函数会接收一个接口参数,基于接口的值以及它的具体类型,我们会创建对应的SQL语句。反射包正好能帮助我们做到这一点。

在写我们的程序之前,我们要先了解一下反射包中的几个类以及相关的方法。我们先一个个看下。

reflect.Type 和reflect.Value

reflect.Type可以代表interface{}的具体类型,reflect.Value可以代表它的值。有俩个函数 reflect.TypeOf()和reflect.ValueOf()他们各自返回了reflect.Type和reflect.Value。
我们来写段程序理解下这俩个类型。

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)

}

上面的这个程序中,createQuery函数接收了一个interface{}作为参数。reflect.TypeOf(q)接收一个interface{}参数,并返回它的此接口的具体数据类型。同样, reflect.ValueOf 接收了一个interface{}作为参数,也返回了此接口参数的实际值。
程序输出如下:

Type  main.order
Value  {456 56}

从上面的输出中,可以看到程序可以打印出接口的对应的实际类型以及实际值。

reflect.Kind

反射包中还有一个重要的类,那就是Kind。

反射包中的Kind和Type看着很像,其实他们还是有些不同,我们从下面这个程序中就可以看出来。

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

我想现在你这时已经很清楚地知道了这俩个的不同之处。Type代表的是接口的实际类型,这里即:main.Order,而Kind代表的是类型的种类,此处即为:struct。

NumField()和Field()方法

NumFiled()方法会返回结构体中的字段的个数,而Field(i int)方法会返回i字段的reflect.Value的值。

package main

import (
    "fmt"
    "reflect"
)

type order struct {
    ordId      int
    customerId int
}

func createQuery(q interface{}) {
    if reflect.ValueOf(q).Kind() == reflect.Struct {
        v := reflect.ValueOf(q)
        fmt.Println("Number of fields", v.NumField())
        for i := 0; i < v.NumField(); i++ {
            fmt.Printf("Field:%d type:%T value:%v\n", i, v.Field(i), v.Field(i))
        }
    }

}
func main() {
    o := order{
        ordId:      456,
        customerId: 56,
    }
    createQuery(o)
}

在上面的程序中,我们首先检查了q的Kind是一个struct,因为NumFiled方法只对strcut有用。程序的其余部分都比较好理解。程序的输出如下:

Number of fields 2
Field:0 type:reflect.Value value:456
Field:1 type:reflect.Value value:56

Int()和String()方法

Int方法和String()方法可用以提取int64和string的reflect.Value的值。

package main

import (
    "fmt"
    "reflect"
)

func main() {
    a := 56
    x := reflect.ValueOf(a).Int()
    fmt.Printf("type:%T value:%v\n", x, x)
    b := "Naveen"
    y := reflect.ValueOf(b).String()
    fmt.Printf("type:%T value:%v\n", y, y)

}

在上面的程序中,我们提取了int64的reflect.Value的值,以及String的reflect.Value的值。程序输出如下:

type:int64 value:56
type:string value:Naveen

完整的程序

现在我们已经知道了如何使用反射,那么我们就来完成我们此前的程序。

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{}) {
    if reflect.ValueOf(q).Kind() == reflect.Struct {
        t := reflect.TypeOf(q).Name()
        query := fmt.Sprintf("insert into %s values(", t)
        v := reflect.ValueOf(q)
        for i := 0; i < v.NumField(); i++ {
            switch v.Field(i).Kind() {
            case reflect.Int:
                if i == 0 {
                    query = fmt.Sprintf("%s%d", query, v.Field(i).Int())
                } else {
                    query = fmt.Sprintf("%s, %d", query, v.Field(i).Int())
                }
            case reflect.String:
                if i == 0 {
                    query = fmt.Sprintf("%s\"%s\"", query, v.Field(i).String())
                } else {
                    query = fmt.Sprintf("%s, \"%s\"", query, v.Field(i).String())
                }
            default:
                fmt.Println("Unsupported type")
                return
            }
        }
        query = fmt.Sprintf("%s)", query)
        fmt.Println(query)
        return

    }
    fmt.Println("unsupported type")
}

func main() {
    o := order{
        ordId:      456,
        customerId: 56,
    }
    createQuery(o)

    e := employee{
        name:    "Naveen",
        id:      565,
        address: "Coimbatore",
        salary:  90000,
        country: "India",
    }
    createQuery(e)
    i := 90
    createQuery(i)

}

程序的输出如下:

insert into order values(456, 56)
insert into employee values("Naveen", 565, "Coimbatore", 90000, "India")
unsupported type

应该使用反射吗?

我们已经展示了如何在实际中使用反射。现在有一个问题。我们应该使用反射吗?这里我引用一下Rob Pike的格言:

Clear is better than clever. Reflection is never clear.

反射很强大,也是Go的一个高级的特性。用它的时候需要特别小心。如果使用反射的话,我们写的代码就不是那么好理解,好维护。我们应尽力避免使用反射,除非必要时。

感谢您的阅读,请留下您珍贵的反馈和评论。Have a good Day!

备注
本文系翻译之作原文博客地址

相关文章

  • Go教程第二十五篇:Go中的反射

    Go中的反射 本文是《Go系列教程》的第二十六篇文章。 反射是Go的高级特性之一。我将尽力讲解地简单些。 本教程具...

  • Go教程第十五篇: 接口-1

    Go教程第十五篇: 接口-1 本文是《Go系列教程》的第十五篇文章,本章我们将讲解接口的第一部分。 什么是接口? ...

  • Go教程第十九篇: Channels

    Go教程第十九篇: Channels 本文是《Go系列教程》的第十九篇文章。 在上一篇文章中,我们讨论了并发是如何...

  • Go教程第五篇:常量

    Go教程第五篇:常量 本文是《Go系列教程》的第五篇 定义 Go里面的常量术语是用于表示固定值,例如: 5,-89...

  • Go教程第二十四篇:结构体而非类

    Go中的面向对象:结构体而非类 本文是《Go系列教程》的第二十四篇文章。 Go是面向对象的吗? Go并不是一个纯面...

  • Go教程第十八篇: Goroutine

    Go教程第十八篇: Goroutine 本文是《Go系列教程》的第十八篇文章。在上一节中,我们讨论了并发的概念以及...

  • Go教程第十六篇: 接口-2

    Go教程第十六篇: 接口-2 本文是《Go系列教程》的第十六篇文章,本章我们将讲解接口的第二部分。 使用指针接收者...

  • Go教程第二十九篇:自定义错误

    Custom Errors 本文是《Go系列教程》的第二十九篇文章。在上篇文章中,我们学习了在Go中如何表示错误以...

  • 笨办法学golang(二)

    这是Go语言学习笔记的第二篇文章。 Go语言学习笔记参考书籍「Go语言编程」、Go官方标准库 前文提要 上篇文章中...

  • go语言学习资料

    菜鸟教程-Go 语言教程 https://www.runoob.com/go/go-tutorial.html老男...

网友评论

      本文标题:Go教程第二十五篇:Go中的反射

      本文链接:https://www.haomeiwen.com/subject/gfgvohtx.html