Go语言基础之反射

Go语言 reflect 包提供反射功能。

它定义了两个重要的 reflect.Type 和 reflect.Value 接口。

并且提供了 reflect.TypeOf 和 reflect.ValueOf 两个函数来获取任意对象的 Value 和 Type。

  1. 大多数现代的高级语言都以各种形式支持反射功能:

    • C/C++ 语言没有支持反射功能,只能通过 typeid 提供非常弱化的程序运行时类型信息;
    • Java、C# 语言都支持完整的反射功能;
    • Lua、JavaScript 类动态语言,由于其本身的语法特性就可以让代码在运行期访问程序自身的值和类型信息,因此不需要反射系统。
  2. 反射是把双刃剑,通过反射可以获取丰富的类型信息,并可以利用这些类型信息做非常灵活的工作。虽然功能强大但代码可读性并不理想,若非必要并不推荐使用反射。

  3. Go语言使用 reflect 包来完成反射机制,提供一种机制在运行时更新和检查变量的值、调用变量的方法和变量支持的内在操作,但是在编译时并不知道这些变量的具体类型,这种机制被称为反射。反射也可以让我们将类型本身作为第一类的值类型处理。

1. 反射的基本概念

反射是指在程序运行期对程序本身进行访问和修改的能力,程序在编译时变量被转换为内存地址,变量名不会被编译器写入到可执行部分,在运行程序时程序无法获取自身的信息。

支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。

Go语言程序的反射系统无法获取到一个可执行文件空间中或者是一个包中的所有类型信息,需要配合使用标准库中对应的词法、语法解析器和抽象语法树(AST)对源码进行扫描后获得这些信息。

反射允许程序运行时对程序本身进行访问和修改的能力。反射主要是空接口(interface{})存储数据上,空接口(interface{})可以表达任意类型的数据,那我们如何知道这个空接口保存的数据值和数据类型了?反射就是在运行时动态的获取一个变量的类型和值。

在Go的反射定义中,任何接口都会由两部分组成的,一个是接口的具体类型,一个是具体类型对应的值。Golang中的reflect包实现了运行时反射,通过调用TypeOf函数返回一个Type类型值,该值代表运行时的数据类型,调用ValueOf函数返回一个Value类型值,该值代表运行时的数据。

2. reflect包

在Go语言的反射机制中,任何接口值都由是一个具体类型具体类型的值两部分组成的(我们在上一篇接口的博客中有介绍相关概念)。 在Go语言中反射的相关功能由内置的reflect包提供,任意接口值在反射中都可以理解为由reflect.Typereflect.Value两部分组成,并且reflect包提供了reflect.TypeOfreflect.ValueOf两个函数来获取任意对象的Value和Type。

2.1 TypeOf

在Go语言中,使用reflect.TypeOf()函数可以获得任意值的类型对象(reflect.Type),程序通过类型对象可以访问任意值的类型信息。

该函数获得任意值的Type对象:

func TypeOf(i interface{}) Type 

Type和Kind

在反射中关于类型还划分为两种:类型(Type)种类(Kind)。因为在Go语言中我们可以使用type关键字构造很多自定义类型,而种类(Kind)就是指底层的类型,但在反射中,当需要区分指针、结构体等大品种的类型时,就会用到种类(Kind)。 举个例子,我们定义了两个指针类型和两个结构体类型,通过反射查看它们的类型和种类。

Type()、Kind()

package main
import (
"fmt"
"reflect"
)
// 定义一个Enum类型
type Enum int
const (
Zero Enum = 0
)
func main() {
// 声明一个空结构体
type cat struct {
}
// 获取结构体实例的反射类型对象
typeOfCat := reflect.TypeOf(cat{})
// 显示反射类型对象的名称和种类
fmt.Println(typeOfCat.Name(), typeOfCat.Kind()) // cat struct
// 获取Zero常量的反射类型对象
typeOfA := reflect.TypeOf(Zero)
// 显示反射类型对象的名称和种类
fmt.Println(typeOfA.Name(), typeOfA.Kind()) // Enum int
}

在使用反射时,需要首先理解类型(Type)和种类(Kind)的区别。

Type

定义:types.Type,指的是系统原生数据类型,和 type 关键字定义的类型,这些类型的名称就是其类型本身的名称。

package main
import (
"fmt"
"reflect"
)
type myInt int64
func reflectType(x interface{}) {
t := reflect.TypeOf(x)
// 比如:Cat的结构体的name是Cat,kind是struct
fmt.Printf("type:%v kind:%v\n", t.Name(), t.Kind())
}
func main() {
var a *float32 // 指针
var b myInt // 自定义类型
var c rune // 类型别名
reflectType(a) // type: kind:ptr
reflectType(b) // type:myInt kind:int64
reflectType(c) // type:int32 kind:int32
type person struct {
name string
age int
}
type book struct{ title string }
var d = person{
name: "沙河小王子",
age: 18,
}
var e = book{title: "《跟小王子学Go语言》"}
reflectType(d) // type:person kind:struct
reflectType(e) // type:book kind:struct
}

Go语言的反射中像数组、切片、Map、指针等类型的变量,它们的.Name()都是返回

Kind

定义:reflection.Kind,指的是对象归属的种类。

type Kind uint
const (
Invalid Kind = iota // 非法类型
Bool // 布尔型
Int // 有符号整型
Int8 // 有符号8位整型
Int16 // 有符号16位整型
Int32 // 有符号32位整型
Int64 // 有符号64位整型
Uint // 无符号整型
Uint8 // 无符号8位整型
Uint16 // 无符号16位整型
Uint32 // 无符号32位整型
Uint64 // 无符号64位整型
Uintptr // 指针
Float32 // 单精度浮点数
Float64 // 双精度浮点数
Complex64 // 64位复数类型
Complex128 // 128位复数类型
Array // 数组
Chan // 通道
Func // 函数
Interface // 接口
Map // 映射
Ptr // 指针
Slice // 切片
String // 字符串
Struct // 结构体
UnsafePointer // 底层指针:直接对指针进行操作(不安全)
)

Elem()

获取指针指向的元素类型,这个获取过程被称为取元素,等效于对指针类型变量*操作。

package main
import (
"fmt"
"reflect"
)
func main() {
// 声明一个空结构体
type cat struct {
}
// 创建cat的实例
ins := &cat{}
// 获取结构体实例的反射类型对象
typeOfCat := reflect.TypeOf(ins)
// 显示反射类型对象的名称和种类
fmt.Printf("name:'%v' kind:'%v'\n", typeOfCat.Name(), typeOfCat.Kind())
// 取类型的元素
typeOfCat = typeOfCat.Elem()
// 显示反射类型对象的名称和种类
fmt.Printf("element name: '%v', element kind: '%v'\n", typeOfCat.Name(), typeOfCat.Kind())
}

// log
name:'' kind:'ptr'
element name: 'cat', element kind: 'struct'

NumField()、FieId~()

反射类型对象(reflect.Type)提供对结构体访问的方法:

package main
import (
"fmt"
"reflect"
)
func main() {
// 声明一个空结构体
type cat struct {
Name string
// 带有结构体tag的字段
Type int `json:"type" id:"100"`
}
// 创建cat的实例
ins := cat{Name: "mimi", Type: 1}
// 获取结构体实例的反射类型对象
typeOfCat := reflect.TypeOf(ins)
// 遍历结构体所有成员
for i := 0; i < typeOfCat.NumField(); i++ {
// 获取每个成员的结构体字段类型
fieldType := typeOfCat.Field(i)
// 输出成员名和tag
fmt.Printf("name: %v tag: '%v'\n", fieldType.Name, fieldType.Tag)
}
// 通过字段名, 找到字段类型信息
if catType, ok := typeOfCat.FieldByName("Type"); ok {
// 从tag中取出需要的tag
fmt.Println(catType.Tag.Get("json"), catType.Tag.Get("id"))
}
}
// log
name: Name tag: ''
name: Type tag: 'json:"type" id:"100"'
type 100

StructField

Field()、FieldByName()、FieldByNameFunc() 等方法返回 StructField 结构,通过这个信息可以获取成员与结构体的关系,如偏移、索引、是否为匿名字段、结构体标签(StructTag)等,而且还可以通过 StructField 的 Type 字段进一步获取结构体成员的类型信息。

type StructField struct {
Name string // 字段名
PkgPath string // 字段路径
Type Type // 字段反射类型对象
Tag StructTag // 字段的结构体标签
Offset uintptr // 字段在结构体中的相对偏移
Index []int // Type.FieldByIndex中的返回的索引值
Anonymous bool // 是否为匿名字段
}

StructTag 结构体标

  1. reflect.StructField 结构中的 Tag 被称为结构体标签(Struct Tag),结构体标签是对结构体字段的额外信息标签。

  2. JSON、Gorm 等进行序列化及对象关系映射(Object Relational Mapping,简称 ORM)系统都会用到结构体标签,这些系统使用标签设定字段在处理时应该具备的特殊属性和可能发生的行为。这些信息都是静态的,无须实例化结构体,可以通过反射获取到。

  3. 编写 Tag 时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,

格式

`key1:"value1" key2:"value2"`

解析与提取

根据键获取对应的值:

func (tag StructTag) Get(key string) string

查询键是否存在:

func (tag StructTag) Lookup(key string) (value string, ok bool)

示例:

package main

import (
"fmt"
"reflect"
)

func main() {
type TextStruct struct{
Text string `remarks:"longText" version:"2" json: "type"`
}

if f,b := reflect.TypeOf(TextStruct{}).FieldByName("Text"); b {
fmt.Println(f.Tag.Get("remarks")) // longText
fmt.Println(f.Tag.Lookup("version")) // 2 true
// 不符合规则,多了一个空格
fmt.Println(f.Tag.Lookup("json")) // false
}
}

2.2 ValueOf

reflect.ValueOf()返回的是reflect.Value类型,其中包含了原始值的值信息。reflect.Value与原始值之间可以互相转换。

该函数获得值的Value对象,通常用来动态地获取或者设置变量的值。

func ValueOf(i interface{}) Value 

reflect.Value类型提供的获取原始值的方法如下:

通过反射获取值

func reflectValue(x interface{}) {
v := reflect.ValueOf(x)
k := v.Kind()// 得到值的种类
switch k {
case reflect.Int64:
// v.Int()从反射中获取整型的原始值,然后通过int64()强制类型转换
fmt.Printf("type is int64, value is %d\n", int64(v.Int()))
case reflect.Float32:
// v.Float()从反射中获取浮点型的原始值,然后通过float32()强制类型转换
fmt.Printf("type is float32, value is %f\n", float32(v.Float()))
case reflect.Float64:
// v.Float()从反射中获取浮点型的原始值,然后通过float64()强制类型转换
fmt.Printf("type is float64, value is %f\n", float64(v.Float()))
}
}
func main() {
var a float32 = 3.14
var b int64 = 100
reflectValue(a) // type is float32, value is 3.140000
reflectValue(b) // type is int64, value is 100
// 将int类型的原始值转换为reflect.Value类型
c := reflect.ValueOf(10)
fmt.Printf("type c :%T\n", c) // type c :reflect.Value
}

通过反射设置变量的值

想要在函数中通过反射修改变量的值,需要注意函数参数传递的是值拷贝,必须传递变量地址才能修改变量值。

判定及获取元素的相关方法

示例:

x := 2 // value type variable?
a := reflect.ValueOf(2) // 2 int no
b := reflect.ValueOf(x) // 2 int no
c := reflect.ValueOf(&x) // &x *int no
d := c.Elem() // 2 int yes


fmt.Println(a.CanAddr()) // "false"
fmt.Println(b.CanAddr()) // "false"
fmt.Println(c.CanAddr()) // "false"
fmt.Println(d.CanAddr()) // "true"

修改值

示例

  1. 值可修改条件:可被寻址、已经导出。

  2. 值的修改从表面意义上叫可寻址,换一种说法就是值必须“可被设置”。那么,想修改变量值,一般的步骤是:

    • 取这个变量的地址或者这个变量所在的结构体已经是指针类型。
    • 使用 reflect.ValueOf 进行值包装。
    • 通过 Value.Elem() 获得指针值指向的元素值对象(Value),因为值对象(Value)内部对象为指针时,使用 set 设置时会报出宕机错误。
    • 使用 Value.Set 设置值。
可被寻址
package main
import (
"reflect"
)
func main() {
// 声明整型变量a并赋初值
var a int = 1024
// 获取变量a的反射值对象
valueOfA := reflect.ValueOf(a)
// 尝试将a修改为1(此处会发生崩溃)
valueOfA.SetInt(1)
}
程序运行崩溃:panic: reflect: reflect.Value.SetInt using unaddressable value

报错的大意是:SetInt 正在使用一个不能被寻址的值。从 reflect.ValueOf 传入的是 a 的值,而不是 a 的地址,这个 reflect.Value 当然是不能被寻址的。将代码修改一下,重新运行:

package main
import (
"fmt"
"reflect"
)
func main() {
// 声明整型变量a并赋初值
var a int = 1024
// 获取变量a的反射值对象(a的地址)
valueOfA := reflect.ValueOf(&a)
// 取出a地址的元素(a的值)
valueOfA = valueOfA.Elem()
// 修改a的值为1
valueOfA.SetInt(1)
// 打印a的值
fmt.Println(valueOfA.Int())
}

当 reflect.Value 不可寻址时,使用 Addr() 方法也是无法取到值的地址的,同时会发生宕机。虽然说 reflect.Value 的 Addr() 方法类似于语言层的&操作;Elem() 方法类似于语言层的*操作,但并不代表这些方法与语言层操作等效。

已经导出

结构体成员中,如果字段没有被导出,即便不使用反射也可以被访问,但不能通过反射修改。

package main
import (
"reflect"
)
func main() {
type dog struct {
legCount int
}
// 获取dog实例的反射值对象
valueOfDog := reflect.ValueOf(&dog{})
// 取出dog实例地址的元素
valueOfDog = valueOfDog.Elem()
// 获取legCount字段的值
vLegCount := valueOfDog.FieldByName("legCount")
// 尝试设置legCount的值(这里会发生崩溃)
vLegCount.SetInt(4)
}

程序运行崩溃:panic: reflect: reflect.Value.SetInt using value obtained using unexported field

报错的意思是:SetInt() 使用的值来自于一个未导出的字段。

isNil()和isValid()

IsNil() ;IsValid() 常被用于判定返回值是否有效。

进行零值和空判定:

isNil()

func (v Value) IsNil() bool

IsNil()报告v持有的值是否为nil。v持有的值的分类必须是通道、函数、接口、映射、指针、切片之一;否则IsNil函数会导致panic。

isValid()

func (v Value) IsValid() bool

IsValid()返回v是否持有一个值。如果v是Value零值会返回假,此时v除了IsValid、String、Kind之外的方法都会导致panic。

举个例子

IsNil()常被用于判断指针是否为空;IsValid()常被用于判定返回值是否有效。

package main
import (
"fmt"
"reflect"
)
func main() {
// *int的空指针
var a *int
fmt.Println("var a *int:", reflect.ValueOf(a).IsNil())
// nil值
fmt.Println("nil:", reflect.ValueOf(nil).IsValid())
// *int类型的空指针
fmt.Println("(*int)(nil):", reflect.ValueOf((*int)(nil)).Elem().IsValid())
// 实例化一个结构体
s := struct{}{}
// 尝试从结构体中查找一个不存在的字段
fmt.Println("不存在的结构体成员:", reflect.ValueOf(s).FieldByName("").IsValid())
// 尝试从结构体中查找一个不存在的方法
fmt.Println("不存在的结构体方法:", reflect.ValueOf(s).MethodByName("").IsValid())
// 实例化一个map
m := map[int]int{}
// 尝试从map中查找一个不存在的键
fmt.Println("不存在的键:", reflect.ValueOf(m).MapIndex(reflect.ValueOf(3)).IsValid())
}
// log
var a *int: true
nil: false
(*int)(nil): false
不存在的结构体成员: false
不存在的结构体方法: false
不存在的键: false

NumField()、Field~()

反射值对象(reflect.Value)提供对结构体访问的方法:

package main
import (
"fmt"
"reflect"
)
// 定义结构体
type dummy struct {
a int
b string
float32
bool
next *dummy
}
func main() {
// 值包装结构体
d := reflect.ValueOf(dummy{
next: &dummy{},
})
// 获取字段数量
fmt.Println(d.NumField())
// 获取索引为2的字段(float32字段)
floatField := d.Field(2)
// 输出字段类型
fmt.Println(floatField.Type())
// 根据名字查找字段
fmt.Println(d.FieldByName("b").Type())
// 根据索引查找值中, next字段的int字段的值
fmt.Println(d.FieldByIndex([]int{4, 0}).Type())
}
// log
5
float32
string
int

3. 结构体反射

json包序列化和反序列化也是使用这个实现的

3.1 与结构体相关的方法

任意值通过reflect.TypeOf()获得反射对象信息后,如果它的类型是结构体,可以通过反射值对象(reflect.Type)的NumField()Field()方法获得结构体成员的详细信息。

reflect.Type中与获取结构体成员相关的的方法如下表所示。

3.2 StructField类型

StructField类型用来描述结构体中的一个字段的信息。

StructField的定义如下:

type StructField struct {
// Name是字段的名字。PkgPath是非导出字段的包路径,对导出字段该字段为""。
// 参见http://golang.org/ref/spec#Uniqueness_of_identifiers
Name string
PkgPath string
Type Type // 字段的类型
Tag StructTag // 字段的标签
Offset uintptr // 字段在结构体中的字节偏移量
Index []int // 用于Type.FieldByIndex时的索引切片
Anonymous bool // 是否匿名字段
}

3.3 结构体反射示例

当我们使用反射得到一个结构体数据之后可以通过索引依次获取其字段信息,也可以通过字段名去获取指定的字段信息。

type student struct {
Name string `json:"name"`
Score int `json:"score"`
}
func main() {
stu1 := student{
Name: "小王子",
Score: 90,
}
t := reflect.TypeOf(stu1)
fmt.Println(t.Name(), t.Kind()) // student struct
// 通过for循环遍历结构体的所有字段信息:NumField返回字段数量
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)// Field通过下标获取value值
fmt.Printf("name:%s index:%d type:%v json tag:%v\n",
ield.Name, field.Index, field.Type, field.Tag.Get("json"))
}
// 通过字段名获取指定结构体字段信息
if scoreField, ok := t.FieldByName("Score"); ok {
fmt.Printf("name:%s index:%d type:%v json tag:%v\n",
scoreField.Name, scoreField.Index, scoreField.Type, scoreField.Tag.Get("json"))
}
}

接下来编写一个函数printMethod(s interface{})来遍历打印s包含的方法。

// 给student添加两个方法 Study和Sleep(注意首字母大写)
func (s student) Study() string {
msg := "好好学习,天天向上。"
fmt.Println(msg)
return msg
}
func (s student) Sleep() string {
msg := "好好睡觉,快快长大。"
fmt.Println(msg)
return msg
}
func printMethod(x interface{}) {
t := reflect.TypeOf(x)
v := reflect.ValueOf(x)
fmt.Println(t.NumMethod())
for i := 0; i < v.NumMethod(); i++ {
methodType := v.Method(i).Type()
fmt.Printf("method name:%s\n", t.Method(i).Name)
fmt.Printf("method:%s\n", methodType)
// 通过反射调用方法传递的参数必须是 []reflect.Value 类型
var args = []reflect.Value{}
v.Method(i).Call(args)
}
}

4. 其他

4.1 通过类型信息创建实例

当已知 reflect.Type 时,可以动态地创建这个类型的实例,实例的类型为指针。

例如 reflect.Type 的类型为 int 时,创建 int 的指针,即*int,代码如下:

package main
import (
"fmt"
"reflect"
)
func main() {
var a int
// 取变量a的反射类型对象
typeOfA := reflect.TypeOf(a)
// 根据反射类型对象创建类型实例
aIns := reflect.New(typeOfA)
// 输出Value的类型和种类
fmt.Println(aIns.Type(), aIns.Kind()) // *int ptr
}

4.2 通过反射调用函数

  1. 如果反射值对象(reflect.Value)中值的类型为函数时,可以通过 reflect.Value 调用该函数。
  2. 使用反射调用函数时,需要将参数使用反射值对象的切片 []reflect.Value 构造后传入 Call() 方法中,调用完成时,函数的返回值通过 []reflect.Value 返回。

下面的代码声明一个加法函数,传入两个整型值,返回两个整型值的和。将函数保存到反射值对象(reflect.Value)中,然后将两个整型值构造为反射值对象的切片([]reflect.Value),使用 Call() 方法进行调用。

package main
import (
"fmt"
"reflect"
)
// 普通函数
func add(a, b int) int {
return a + b
}
func main() {
// 将函数包装为反射值对象
funcValue := reflect.ValueOf(add)
// 构造函数参数, 传入两个整型值
paramList := []reflect.Value{reflect.ValueOf(10), reflect.ValueOf(20)}
// 反射调用函数
retList := funcValue.Call(paramList)
// 获取第一个返回值, 取整数值
fmt.Println(retList[0].Int())
}

提示

反射调用函数的过程需要构造大量的 reflect.Value 和中间变量,对函数参数值进行逐一检查,还需要将调用参数复制到调用函数的参数内存中。调用完毕后,还需要将返回值转换为 reflect.Value,用户还需要从中取出调用值。因此,反射调用函数的性能问题尤为突出,不建议大量使用反射函数调用。

总结

反射是一个强大并富有表现力的工具,能让我们写出更灵活的代码。但是反射不应该被滥用,原因有以下三个。

  1. 基于反射的代码是极其脆弱的,反射中的类型错误会在真正运行的时候才会引发panic,那很可能是在代码写完的很长时间之后。

  2. 大量使用反射的代码通常难以理解。

  3. 反射的性能低下,基于反射实现的代码通常比正常代码运行速度慢一到两个数量级。

参考感谢
Go语言基础之反射 · 语雀 (yuque.com)
03.反射介绍 · 语雀 (yuque.com)
反射 · 语雀 (yuque.com)
reflection 反射 · 语雀 (yuque.com)