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

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!

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,951评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,606评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,601评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,478评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,565评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,587评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,590评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,337评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,785评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,096评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,273评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,935评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,578评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,199评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,440评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,163评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,133评论 2 352