在上一讲中,介绍了 Go 语言反射的基本使用和三个定律。本讲继续深入探讨反射的更多用法,具体包含以下内容:

  1. 使用反射访问结构体
    • 获取结构体成员信息
    • 修改结构体成员的值
  2. 使用反射调用函数
  3. 使用反射创建实例

本讲内容不少,闲话不多说,我们直奔主题。

# 使用反射访问结构体

反射能从普通变量中获取类型和值信息,对于结构体来说亦可获取其成员的类型和值。此外,还能获取结构体成员的个数、标签的值。为了讲解方便,我们先定义一个示例结构体,具体如下:

go
复制代码type Person struct {
   Name   string `display:"名字"`
   age    int    `display:"年龄"`
   gender int    `display:"性别"`
}

这个名为 Person 的结构体包含 3 个成员,分别是 string 类型的 Name、int 类型的 age 和 gender。每个成员都包含名为 display 的标签,用于解释它们的含义。

💡 提示:在 Person 中,Name 时大写开头,表示可从外部访问。只有这样的成员,其值才能被修改。

在讨论其成员前,我们不妨先看看下面这段代码会产生怎样的运行结果:

go
复制代码func main() {
   personExample := Person{Name: "小明", age: 18, gender: 1}
   fmt.Println(reflect.TypeOf(personExample).Name())
   fmt.Println(reflect.TypeOf(personExample).Kind())
}

实际上,这段代码是在考察大家对上一讲关于“类型”和“种类”的理解。所有 Person 类型的变量,其类型名都是 Person;从分类上讲,所有 Person 类型的变量都属于结构体分类(struct)。所以,上述代码的运行结果为:

Person

struct

# 获取结构体成员信息

结构体中的成员,在英语中通常被称为“Field”。Go 语言提供了获取成员个数属性信息的方法,它们通过 reflect.Type 类型调用。较为常用的有以下两个方法:

go
复制代码// 获取成员个数
NumField()
// 获取成员属性信息
Field()

将上述两个方法结合使用,可以解读任何一个结构体的全部成员信息。写法也较为通用,具体如下:

go
复制代码func main() {
   personExample := Person{Name: "小明", age: 18, gender: 1}
   typeOfPersonExample := reflect.TypeOf(personExample)
   for i := 0; i < typeOfPersonExample.NumField(); i++ {
      fmt.Println(typeOfPersonExample.Field(i).Name,
         typeOfPersonExample.Field(i).Type,
      )
}

这段代码中使用 for 循环遍历所有的成员,结束循环则以成员个数为条件,因此在循环体内不会发生下标越界的错误。typeOfPersonExample.Field() 方法包含了某个成员的属性信息,类型是 StructField

运行代码后,控制台将输出:

Name string

age int

gender int

显然,成员的名称和类型一览无余。

细心的朋友还会发现,StructField 还提供了其它的信息,其中有名为 Tag 的,它是不是对应着标签呢?答案是肯定的。“实践是检验真理的唯一标准”,我们不妨尝试输出 Tag 的值。将本例的输出语句改为:

go
复制代码fmt.Println(typeOfPersonExample.Field(i).Name,
   typeOfPersonExample.Field(i).Type,
   typeOfPersonExample.Field(i).Tag,
)

再次运行程序,可以看到控制台如下输出:

Name string display:"名字"

age int display:"年龄"

gender int display:"性别"

很显然,输出了完整的标签的内容。

随着标签的获取,问题也随之而来。结构体成员的标签允许多个同时存在,怎样获取某个特定标签的值呢?如果没有某个标签,该怎样处理呢?

要解答这个问题,就要看看获取来的 Tag 是怎样的数据结构了。经过研究可以发现,Tag 的值是 StructTag 类型。对于该类型,对外开放了 2 个方法。一个是 Get(),即直接获取某个标签的值;还有一个是 Lookup(),即查找某个标签。对于前者,直接返回 string 类型的值;对于后者则返回 string 类型的值和 bool 类型的值,查找结果。

对于本例而言,若要精准获取名为 display 的标签的值,方法如下:

go
复制代码func main() {
   personExample := Person{Name: "小明", age: 18, gender: 1}
   typeOfPersonExample := reflect.TypeOf(personExample)
   for i := 0; i < typeOfPersonExample.NumField(); i++ {
      fmt.Println(typeOfPersonExample.Field(i).Tag.Get("display"))
   }
}

运行后,可以看到控制台如下输出:

名字

年龄

性别

💡 提示:标签实际上也是 Key-Value 的键值对结构,较为良好的编码习惯是在直接获取值之前先判断是否存在对应的键。幸运的是,Get() 方法的内部实现就是调用了 Lookup() 方法,在键不存在时返回空字符串( Lookup() 方法在检测不到对应的键时的返回结果)。如此一来,开发者直接调用 Get() 方法即可。
❗️ 注意:标签的内容也是区分大小写的,若将上例中的“display”改为“Display”,则得不到任何标签结果输出。

说完成员的类型、标签的值的获取,接下来便是获取和修改成员的值了。

值的获取和类型的获取方法很类似,只需将获取类型改为获取值就可以了。具体代码如下:

go
复制代码func main() {
   personExample := Person{Name: "小明", age: 18, gender: 1}
   valueOfPersonExample := reflect.ValueOf(personExample)
   for i := 0; i < valueOfPersonExample.NumField(); i++ {
      fmt.Println(valueOfPersonExample.Field(i))
   }
}

运行后,控制台将输出:

小明

18

1

最后,无论是获取类型还是获取值,可以直接调用 FieldByName() 方法精准地获取某个成员的值。下面的代码演示了直接获取本例中 valueOfPersonExample 变量中 Name 成员的值。

go
复制代码func main() {
   personExample := Person{Name: "小明", age: 18, gender: 1}
   valueOfPersonExample := reflect.ValueOf(personExample)
   fmt.Println(valueOfPersonExample.FieldByName("Name"))
}

运行后,控制台将输出“小明”字样。

最后,如果我们想使用反射来修改某个成员的值,也要确保被修改的操作对象必须是可修改的。另外还必须是可从外部访问的(即成员名开头是大写字母)。对于本例而言,只有 Name 成员可被修改。下面的代码演示了如何将“小明”改为“小红”。

go
复制代码func main() {
   personExample := &Person{Name: "小明", age: 18, gender: 1}
   valueOfPersonExample := reflect.ValueOf(personExample).Elem()
   valueOfPersonExample.FieldByName("Name").SetString("小红")
   fmt.Println(valueOfPersonExample.FieldByName("Name"))
}

程序运行后,可以观察到控制台将输出“小红”字样。

# 使用反射调用函数

在 Go 语言中通过反射调用函数,首先要明确一个概念:函数也可当作变量使用

比如:现有一个函数,名为 addCalc(),需要两个 int 参数 num1 和 num2。运行结果就是这两个数的相加和。完整的函数如下:

go
复制代码func addCalc(num1 int, num2 int) int {
   return num1 + num2
}

当我们尝试使用下面的反射方法来获取 addCalc() 函数的类型:

go
复制代码func main() {
   addCalcType := reflect.TypeOf(addCalc)
   fmt.Println(addCalcType.Kind())
}

将得到这样的结果:

func

从结果上看,函数类型被归为了“func”种类

💡 提示:凭借种类获取的结果(“func”),我们可以判断传入的参数是否为函数。

接着,addCalc() 函数需要两个参数,通过 Value 类型的切片数据构建参数列表,具体如下:

go
复制代码addCalcParam := []reflect.Value{reflect.ValueOf(20), reflect.ValueOf(200)}

最后,reflect.Value 类型提供了 Call() 方法,用于调用函数。该方法仅适用于函数,对普通变量或结构体不适用。该方法执行后,也将返回 Value 类型的切片数据,包含着函数运行后的返回结果

对于本例而言,则要首先获取 addCalc() 函数的值(reflect.Value)变量,在通过该变量调用 Call() 方法,传入 addCalcParam,最后即可从该方法结果中获取函数执行的返回值了。完整代码如下:

go
复制代码func main() {
   addCalcValue := reflect.ValueOf(addCalc)
   addCalcParam := []reflect.Value{reflect.ValueOf(20), reflect.ValueOf(200)}
   results := addCalcValue.Call(addCalcParam)
   fmt.Println(results[0])
}

运行后,控制台输出如下:

220

# 使用反射创建实例

通过反射创建实例,通常用于创建一个与已知变量同类型的变量。如此创建的变量类型只有在程序运行时才会被确定,更加灵活多变

举例来说,现有一个变量 num,它的类型是自定义的 myInt 类型。我们若想创建与其相同类型的变量,方法如下:

go
复制代码type myInt int
func main() {
   var num myInt = 100
   typeOfNum := reflect.TypeOf(num)
   anotherNum := reflect.New(typeOfNum)
   anotherNum.Elem().SetInt(300)
   fmt.Println(num)
   fmt.Println(anotherNum.Type(), anotherNum.Type().Kind())
   fmt.Println(anotherNum.Elem().Int())
}

使用反射创建变量,核心在于 reflect.New() 函数。该函数接收 reflect.Type 类型参数,返回 reflect.Value 类型值。该值是一个指针,本例中的 anotherNum 类型实际上是 *main.myInt,从种类上讲是 ptr。

运行这段代码,控制台输出如下:

100

*main.myInt ptr

300

# 小结

🎉 恭喜,您完成了本次课程的学习!

📌 以下是本次课程的重点内容总结:

  1. 使用反射访问结构体
    • 获取结构体成员信息
    • 修改结构体成员的值
  2. 使用反射调用函数
  3. 使用反射创建实例

本讲继续深入探讨 Go 反射的使用。首先是访问结构体和结构体成员的属性信息成员个数、类型、标签及单个标签值的获取)、值以及修改成员的值

接下来介绍了如何使用反射调用函数,这里有个重要的思想:Go 语言中的函数也可以当作变量使用。这一点有点类似 JavaScript,或者是 TypeScript。

看到这,相信不少朋友会问:上述技术的实际应用场景是什么呢?

诚然,反射在实际开发中用到的并不是很广泛。但若使用得当,将起到事半功倍之作用。比如标签的获取,就是 json 解析包的实现的基础。又如:我们可以定义若干结构体,对应数据库的表结构。然后使用反射进行一次编码,即可批量创建全部数据表等等。

随着 Go 使用日益增多,需求各异,像 Nirvana 这类 Http 框架内部就用到了反射调用函数。当然,说到“框架”,最好是基础全学完,再上手的东西了。

最后介绍的是使用反射创建已知类型的实例。这样的创建方式使得变量的类型在程序运行时固定,而非编译时。为构建灵活、多变的程序逻辑提供了更多可能。可以通过不同的条件分支,对相同变量赋不同类型的数据。

➡️ 在下次课程中,我们会收尾 Go 语言中反射的专题,同时也带来了理解难度最大的部分。具体内容如下:

  1. 控制反转与依赖注入