超详细的Go 语言 反射 reflection (带完整示例)
反射是 Go 语言中比较高级的用法之一。 这里我们尽量让它变得简单易懂。
什么是反射(reflection)
反射是程序在运行时检查其变量和值并找到它们的类型的能力。 你可能不明白这意味着什么,但没关系。 在本篇文章结束时,我们将对反射有一个清晰的理解。
检查变量并找到其类型的需要什么?
任何人在学习反射时都会遇到的第一个问题是,当我们程序中的每个变量都由我们定义并且我们在编译时本身就知道它的类型时,为什么我们还要在运行时检查一个变量并找到它的类型呢。 嗯,这在大多数情况下都是不需要的,但事实并非总是如此。
让我们看一个简单的程序。
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)
}
我们需要编写一个函数,将上面程序中的 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 字段创建插入语句。 该程序将输出
现在让我们将查询创建器提升到一个新的水平。 如果我们想泛化我们的查询创建器并使其适用于任何结构体怎么办。 让我解释一下我所说的使用程序是什么意思。
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{} 参数的基础值。
上面的程序打印结果如下
从输出结果中,我们可以看到程序打印出接口的具体类型和值。
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)
}
上面的程序打印结果如下
我想现在我们应该清楚两者之间的差异。 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有效。 该程序的其余部分是很容易理解的。 该程序输出如下
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 中一个非常强大和先进的概念,应该谨慎使用。 使用反射编写清晰且可维护的代码非常困难。 应尽可能避免使用,仅在绝对必要时使用。
相关文章
使用 C 语言中的 goto 语句
发布时间:2023/05/07 浏览次数:79 分类:C语言
-
本文介绍了如何在 C 语言中使用 goto 语句。使用 goto 语句在 C 语言中实现循环 goto 关键字是 C 语言的一部分,它提供了一个做无条件跳转的结构。
Django 中的 Slug
发布时间:2023/05/04 浏览次数:173 分类:Python
-
本篇文章旨在定义一个 slug 以及我们如何使用 slug 字段在 Python 中使用 Django 获得独特的帖子。
在 Django 中按降序过滤查询集中的项目
发布时间:2023/05/04 浏览次数:157 分类:Python
-
在这个讲解中,学习如何借助 Django 中的 order_by() 方法按降序过滤出查询集中的项目。
Django ALLOWED_HOSTS 介绍
发布时间:2023/05/04 浏览次数:181 分类:Python
-
本文展示了如何创建您的 Django 网站,为公开发布做好准备,如何设置 ALLOWED_HOSTS 以及如何在使用 Django 进行 Web 部署期间修复预期的主要问题。
Django 中的 Select_related 方法
发布时间:2023/05/04 浏览次数:129 分类:Python
-
本文介绍了什么是查询集,如何处理这些查询以及我们如何利用 select_related() 方法来过滤 Django 中相关模型的查询。
使用 Post 请求将数据发送到 Django 服务器
发布时间:2023/05/04 浏览次数:159 分类:Python
-
在这篇关于Django的讲解中,我们简要介绍了post和get请求以及如何在Django中用post实现CSRF token。
Django 返回 JSON
发布时间:2023/05/04 浏览次数:106 分类:Python
-
在与我们的讨论中,我们简要介绍了 JSON 格式,并讨论了如何借助 Django 中的 JsonResponse 类将数据返回为 JSON 格式。
在 Django 中创建对象
发布时间:2023/05/04 浏览次数:59 分类:Python
-
本文的目的是解释什么是模型以及如何使用 create() 方法创建对象,并了解如何在 Django 中使用 save() 方法。