迹忆客 专注技术分享

当前位置:主页 > 学无止境 > 编程语言 >

超详细的Go 语言 反射 reflection (带完整示例)

作者:迹忆客 最近更新:2022/01/16 浏览次数:

反射是 Go 语言中比较高级的用法之一。 这里我们尽量让它变得简单易懂。

什么是反射(reflection)

反射是程序在运行时检查其变量和值并找到它们的类型的能力。 你可能不明白这意味着什么,但没关系。 在本篇文章结束时,我们将对反射有一个清晰的理解。

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

任何人在学习反射时都会遇到的第一个问题是,当我们程序中的每个变量都由我们定义并且我们在编译时本身就知道它的类型时,为什么我们还要在运行时检查一个变量并找到它的类型呢。 嗯,这在大多数情况下都是不需要的,但事实并非总是如此。

让我们看一个简单的程序。

package main

import (  
    "fmt"
)

func main() {  
    i := 10
    fmt.Printf("%d %T", i, i)
}

运行示例

在上面的程序中,变量 i 的类型在编译时是已知的,我们在下一行将它打印出来。 这里并没有什么神奇的。

Go 打印变量值和类型

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

package main

import (  
    "fmt"
)

type order struct {  
    ordId      int
    customerId int
}

func main() {  
    o := order{
        ordId:      1234,
        customerId: 567,
    }
    fmt.Println(o)
}

运行示例

我们需要编写一个函数,将上面程序中的 struct 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 字段创建插入语句。 该程序将输出

Go 函数返回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 函数。 以便它可以将任何结构体作为参数并基于结构体字段创建插入语句。

例如,如果我们将下面的结构体作为参数

o := order {  
    ordId: 4321,
    customerId: 765
}

我们的 createQuery 函数应该返回

insert into order values (4321, 765)  

同样,如果我们将下面的结构体作为参数传给 createQuery 函数

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 函数应该适用于任何结构体,因此它需要一个 interface{} 作为参数。 为简单起见,我们将只处理包含 string 和 int 类型字段的结构,但这可以扩展到任何类型。

createQuery 函数应该适用于任何结构体。 编写此函数的唯一方法是检查在运行时传递给它的 struct 参数的类型,找到它的字段,然后创建语句。 这就是反射有用的地方。 在本篇文章的后续步骤中,我们将学习如何使用反射包(reflect package)来实现这一点。

反射包 (reflect package)

reflect 包在 Go 中实现了运行时反射。 reflect 包有助于识别底层的具体类型和 interface{} 变量的值。 这正是我们所需要的。 createQuery 函数采用 interface{} 参数,并且需要根据 interface{} 参数的具体类型和值创建语句。 这正是反射包的作用。

在编写通用查询生成器程序之前,我们需要首先了解反射包中的一些类型和方法。

reflect.Type 和 reflect.Value

interface{} 的具体类型由 reflect.Type 表示,底层值由 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 将 interface{} 作为参数并返回包含传递的 interface{} 参数的具体类型的 reflect.Type。 类似地,reflect.ValueOf 函数将 interface{} 作为参数并返回 reflect.Value,其中包含传递的 interface{} 参数的基础值。

上面的程序打印结果如下

go TypeOf 和 ValueOf

从输出结果中,我们可以看到程序打印出接口的具体类型和值。

reflect.Kind

reflect 包中还有一种更重要的类型,称为 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)

}

运行示例

上面的程序打印结果如下

go kind vs type

我想现在我们应该清楚两者之间的差异。 Type 表示 interface{} 的实际类型,在本例中为 main.Order; Kind 表示该类型的具体类型 - struct。

NumField() 和 Field() 方法

NumField() 方法返回结构体中的字段数,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,因为NumField方法只对struct有效。 该程序的其余部分是很容易理解的。 该程序输出如下

go NumField 和 Field 方法

Int() 和 String() 方法

Int 和 String 方法分别将 reflect.Value 的值提取为 int64 和字符串。

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)

}

运行示例

在上面的程序中我们将 reflect.Value 提取为 int64 。 接着我们使用 String() 方法将其提取为字符串。 该程序执行结果如下

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)

}

运行示例

我们首先检查传入的参数是否是struct。 我们使用 Name() 方法从 reflect.Type 中获取结构体的名称。 然后我们使用 t 并开始创建插入语句。

代码中的 case 语句检查当前字段是否为 reflect.Int,如果是这种情况,我们使用 Int() 方法将该字段的值提取为 int64。 if else 语句用于处理极端情况。 使用类似的逻辑提取字符串。

我们还添加了检查来防止在将不受支持的类型传递给 createQuery 函数时程序崩溃。 该程序的其余部分是容易理解的。 建议在适当的地方添加日志来检查它们的输出从而可以更好地理解这个程序。

上面的程序执行结果如下

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

感兴趣的读者可以将字段名称添加到输出查询中。 可以尝试更改程序来打印以下的插入语句

insert into order(ordId, customerId) values(456, 56)  

我们可以在 Go 在线工具 中编写代码来查看结果


应该使用反射(reflect)吗?

在了解了反射的实际用途之后,现在是真正的问题。 应该使用反射吗? 我想引用 Rob Pike 关于使用反射的说法来回答这个问题。

Clear is better than clever. Reflection is never clear.

反射是 Go 中一个非常强大和先进的概念,应该谨慎使用。 使用反射编写清晰且可维护的代码非常困难。 应尽可能避免使用,仅在绝对必要时使用。

除非注明转载,本站文章均为原创或翻译,欢迎转载,转载请以链接形式注明出处

本文地址:

迹忆客

专注技术分享,项目实战分享!

技术宅 乐于分享 7年编程经验
社交账号
  • https://www.github.com/onmpw
  • qq:1244347461

热门文章

教程更新

热门标签

Go
扫码一下
查看教程更方便