[译] part 34: golang 反射 reflection

https://juejin.im/post/5cad938ae51d456e3f2fb6cd

反射是 Go 的高级主题之一,我会尽量让它变得简单。

本教程包含以下部分。

  • 什么是反射?
  • 检查变量并找到其类型需要做什么?
  • reflect
    • reflect.Typereflect.Value
    • reflect.Kind
    • NumField()Field() 方法
    • Int()String() 方法
  • 完整的程序
  • 应该使用反射吗?

我们现在一个一个来讨论这些部分。

什么是反射

反射是为了程序在运行时检查其变量和值并找到其类型。你可能不明白这意味着什么,但没关系。在本教程结束时,您将清楚地了解反射,请跟紧我。

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

在学习反射时,任何人都会想到的第一个问题是,为什么我们需要检查变量并在运行时找到它的类型,因为我们的程序中的每个变量都由我们定义的,我们在编译时就知道它的类型。嗯,大部分时间都是如此,但并非总是如此。

我们用一个简单的程序来解释一下我的意思。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. i := 10
  7. fmt.Printf("%d %T", i, i)
  8. }

Run in playgound

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

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

看看下的代码,

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type order struct {
  6. ordId int
  7. customerId int
  8. }
  9. func main() {
  10. o := order{
  11. ordId: 1234,
  12. customerId: 567,
  13. }
  14. fmt.Println(o)
  15. }

Run in playground

我们需要编写一个函数,它将上面程序中的结构 o 作为参数并返回以下 SQL 插入语句,

  1. insert into order values(1234, 567)

这个功能很容易实现。让我们现在就搞。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type order struct {
  6. ordId int
  7. customerId int
  8. }
  9. func createQuery(o order) string {
  10. i := fmt.Sprintf("insert into order values(%d, %d)", o.ordId, o.customerId)
  11. return i
  12. }
  13. func main() {
  14. o := order{
  15. ordId: 1234,
  16. customerId: 567,
  17. }
  18. fmt.Println(createQuery(o))
  19. }

Run in playground

第 12 行中的createQuery函数,使用oordIdcustomerId字段创建插入查询。该程序将输出,

  1. insert into order values(1234, 567)

现在让我们的createQuery进入下一个级别。如果我们想要概括我们的createQuery并使其适用于任何结构,该怎么办?让我解释一下我使用程序的意思。

  1. package main
  2. type order struct {
  3. ordId int
  4. customerId int
  5. }
  6. type employee struct {
  7. name string
  8. id int
  9. address string
  10. salary int
  11. country string
  12. }
  13. func createQuery(q interface{}) string {
  14. }
  15. func main() {
  16. }

我们的目标是完成以上程序第 16 行的createQuery函数,以便它将任何结构作为参数,并基于结构字段创建插入查询。

例如,如果我们传递下面的结构,

  1. o := order {
  2. ordId: 1234,
  3. customerId: 567
  4. }

我们的createQuery函数应该返回,

  1. insert into order values (1234, 567)

同样的我们传下面的结构,

  1. e := employee {
  2. name: "Naveen",
  3. id: 565,
  4. address: "Science Park Road, Singapore",
  5. salary: 90000,
  6. country: "Singapore",
  7. }

应该返回,

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

由于createQuery函数应该与任何结构一起使用,因此它将interface{}作为参数。为简单起见,我们只处理包含stringint类型字段的结构,但这可以扩展为任何类型。

createQuery函数应该适用于任何结构。编写此函数的唯一方法是检查在运行时传递给它的结构参数的类型,找到它的字段然后创建查询。这是应用反射的地方。在本教程的后续步骤中,我们将学习如何使用reflect包实现此目的。

reflect

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

在编写我们的通用查询生成器程序之前,我们首先需要知道reflect 包中的一些类型和方法。让我们逐一看看它们。

reflect.Typereflect.Value

interface {}的具体类型由reflect.Type表示,底层值由reflect.Value表示。有两个函数reflect.TypeOf()reflect.ValueOf(),它们分别返回reflect.Typereflect.Value。这两种类型是创建查询生成器的基础。让我们写一个简单的例子来理解这两种类型。

  1. package main
  2. import (
  3. "fmt"
  4. "reflect"
  5. )
  6. type order struct {
  7. ordId int
  8. customerId int
  9. }
  10. func createQuery(q interface{}) {
  11. t := reflect.TypeOf(q)
  12. v := reflect.ValueOf(q)
  13. fmt.Println("Type ", t)
  14. fmt.Println("Value ", v)
  15. }
  16. func main() {
  17. o := order{
  18. ordId: 456,
  19. customerId: 56,
  20. }
  21. createQuery(o)
  22. }

Run in playground

在上面的程序中,第 13 行的createQuery函数以interface {}作为参数。第 14 行的函数reflect.TypeOfinterface {}作为参数,并返回包含传递的interface {}参数的具体类型的reflect.Type。类似,第 15 行中的·reflect.ValueOf·函数也将interface {}作为参数并返回reflect.Value,其中包含传递的interface {}参数的基础值。

上述程序打印,

  1. Type main.order
  2. Value {456 56}

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

reflect.Kind

反射包中有一个更重要的类型叫Kind

反射包中的KindType可能看起来相似,但它们之间存在差异,这将从下面的程序中清楚地看出。

  1. package main
  2. import (
  3. "fmt"
  4. "reflect"
  5. )
  6. type order struct {
  7. ordId int
  8. customerId int
  9. }
  10. func createQuery(q interface{}) {
  11. t := reflect.TypeOf(q)
  12. k := t.Kind()
  13. fmt.Println("Type ", t)
  14. fmt.Println("Kind ", k)
  15. }
  16. func main() {
  17. o := order{
  18. ordId: 456,
  19. customerId: 56,
  20. }
  21. createQuery(o)
  22. }

Run in playground

上述程序输出,

  1. Type main.order
  2. Kind struct

我想你现在会清楚两者之间的差异。 Type表示interface {}的实际类型,在这个例子中,它是main.Order。而Kind表示类型的特定种类。在这个例子中,它是一个struct

NumField()Field()方法

NumField()方法返回结构中的字段数,Field(i int)方法返回第i个字段的reflect.Value

  1. package main
  2. import (
  3. "fmt"
  4. "reflect"
  5. )
  6. type order struct {
  7. ordId int
  8. customerId int
  9. }
  10. func createQuery(q interface{}) {
  11. if reflect.ValueOf(q).Kind() == reflect.Struct {
  12. v := reflect.ValueOf(q)
  13. fmt.Println("Number of fields", v.NumField())
  14. for i := 0; i < v.NumField(); i++ {
  15. fmt.Printf("Field:%d type:%T value:%v\n", i, v.Field(i), v.Field(i))
  16. }
  17. }
  18. }
  19. func main() {
  20. o := order{
  21. ordId: 456,
  22. customerId: 56,
  23. }
  24. createQuery(o)
  25. }

Run in playground

在上面的程序中的第 14 行,我们首先检查qKind是否是结构,因为NumField方法仅适用于结构。该代码的其余部分比较好懂,该程序输出,

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

Int()String()方法

IntString方法有助于将reflect.Value分别提取为int64string类型。

  1. package main
  2. import (
  3. "fmt"
  4. "reflect"
  5. )
  6. func main() {
  7. a := 56
  8. x := reflect.ValueOf(a).Int()
  9. fmt.Printf("type:%T value:%v\n", x, x)
  10. b := "Naveen"
  11. y := reflect.ValueOf(b).String()
  12. fmt.Printf("type:%T value:%v\n", y, y)
  13. }

Run in playground

在上面的程序中的第 10 行,我们将reflect.Value提取为int64,在第 13 行,我们把它作为string提取出来。这个程序打印,

  1. type:int64 value:56
  2. type:string value:Naveen

完整的程序

现在我们有足够的知识来完成我们的查询生成器,那就让我们继续吧。

  1. package main
  2. import (
  3. "fmt"
  4. "reflect"
  5. )
  6. type order struct {
  7. ordId int
  8. customerId int
  9. }
  10. type employee struct {
  11. name string
  12. id int
  13. address string
  14. salary int
  15. country string
  16. }
  17. func createQuery(q interface{}) {
  18. if reflect.ValueOf(q).Kind() == reflect.Struct {
  19. t := reflect.TypeOf(q).Name()
  20. query := fmt.Sprintf("insert into %s values(", t)
  21. v := reflect.ValueOf(q)
  22. for i := 0; i < v.NumField(); i++ {
  23. switch v.Field(i).Kind() {
  24. case reflect.Int:
  25. if i == 0 {
  26. query = fmt.Sprintf("%s%d", query, v.Field(i).Int())
  27. } else {
  28. query = fmt.Sprintf("%s, %d", query, v.Field(i).Int())
  29. }
  30. case reflect.String:
  31. if i == 0 {
  32. query = fmt.Sprintf("%s\"%s\"", query, v.Field(i).String())
  33. } else {
  34. query = fmt.Sprintf("%s, \"%s\"", query, v.Field(i).String())
  35. }
  36. default:
  37. fmt.Println("Unsupported type")
  38. return
  39. }
  40. }
  41. query = fmt.Sprintf("%s)", query)
  42. fmt.Println(query)
  43. return
  44. }
  45. fmt.Println("unsupported type")
  46. }
  47. func main() {
  48. o := order{
  49. ordId: 456,
  50. customerId: 56,
  51. }
  52. createQuery(o)
  53. e := employee{
  54. name: "Naveen",
  55. id: 565,
  56. address: "Coimbatore",
  57. salary: 90000,
  58. country: "India",
  59. }
  60. createQuery(e)
  61. i := 90
  62. createQuery(i)
  63. }

Run in playground

在第 22 行,我们首先检查传递的参数是否为结构。然后我们使用Name()方法从reflect.Type获取结构的名称。在下一行中,我们使用t并开始创建查询。

第 28 行,case语句检查当前字段是否为reflect.Int,如果是的话,我们使用Int()方法将该字段的值提取为int64if else语句用于处理边缘情况,请添加日志以了解为何需要它。类似的逻辑用于提取string

我们还添加了一些检查,以防止在将不支持的类型传递给createQuery函数时导致程序崩溃。该程序的其他代码也比较好懂,建议在适当的位置添加日志并检查其输出以更好地理解该程序。

程序输出,

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

留下一个练习给读者,如果我们要将字段名称添加到输出查询中,该怎么修改?请尝试更改程序以打印如下格式的查询,

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

应该使用反射吗

我们展示了一个反射的用途。现在有一个实际的问题,你应该使用反射吗?我想引用 Rob Pike 关于使用反射的回答来解释这个问题。

Clear is better than clever. Reflection is never clear. — 清晰比聪明更好,但是反射不清晰

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

如果喜欢我的教程,可以向我捐赠)。您的捐款将帮助我创建更多精彩的教程。

ft_authoradmin  ft_create_time2019-08-03 16:41
 ft_update_time2019-08-03 16:42