反射概述
反射是这样一种机制,它是可以让我们在程序运行时(runtime)访问、检测和修改对象本身状态或行为的一种能力。
比如,从一个变量推断出其类型信息、以及存储的数据的一些信息,又或者获取一个对象有什么方法可以调用等。
反射经常用在一些需要同时处理不同类型变量的地方,比如序列化、反序列化、ORM
等等,如标准库里面的 json.Marshal
。
反射基础 - go 的 interface 是怎么存储的?
在正式开始讲解反射之前,我们有必要了解一下 go
里的接口(interface
)是怎么存储的。
关于这个问题,在我的另外一篇文章中已经做了很详细的讲解 go
interface 设计与实现, 这里不再赘述。但还是简单说一下,go
的接口是由两部分组成的,一部分是类型信息,另一部分是数据信息,如:
1 | var a = 1 |
对于这个例子,b
的类型信息是
int
,数据信息是 1
,这两部分信息都是存储在
b
里面的。b
的内存结构如下:
在上图中,b
的类型实际上是
eface
,它是一个空接口,它的定义如下:
1 | type eface struct { |
也就是说,一个 interface{} 中实际上既包含了变量的类型信息,也包含了类型的数据。 正因为如此,我们才可以通过反射来获取到变量的类型信息,以及变量的数据信息。
反射对象 - reflect.Type 和 reflect.Value
知道了 interface{}
的内存结构之后,我们就可以开始讲解反射了。反射的核心是两个对象,分别是
reflect.Type
和 reflect.Value
。 它们分别代表了
go 语言中的类型和值。我们可以通过 reflect.TypeOf
和
reflect.ValueOf
来获取到一个变量的类型和值。
1 | var a = 1 |
我们去看一下 TypeOf
和 ValueOf
的源码会发现,这两个方法都接收一个 interface{}
类型的参数,然后返回一个 reflect.Type
和
reflect.Value
类型的值。这也就是为什么我们可以通过
reflect.TypeOf
和 reflect.ValueOf
来获取到一个变量的类型和值的原因。
反射定律
在 go 官方博客中关于反射的文章 laws-of-reflection 中,提到了三条反射定律:
- 反射可以将
interface
类型变量转换成反射对象。 - 反射可以将反射对象还原成
interface
对象。 - 如果要修改反射对象,那么反射对象必须是可设置的(
CanSet
)。
关于这三条定律,官方博客已经有了比较完整的阐述,感兴趣的可以去看一下官方博客的文章。这里简单阐述一下:
反射可以将
interface
类型变量转换成反射对象。
其实也就是上面的 reflect.Type
和
reflect.Value
,我们可以通过 reflect.TypeOf
和
reflect.ValueOf
来获取到一个变量的反射类型和反射值。
1 | var a = 1 |
反射可以将反射对象还原成
interface
对象。
我们可以通过 reflect.Value.Interface
来获取到反射对象的
interface
对象,也就是传递给 reflect.ValueOf
的那个变量本身。 不过返回值类型是
interface{}
,所以我们需要进行类型断言。
1 | i := valueOfA.Interface() |
如果要修改反射对象,那么反射对象必须是可设置的(CanSet
)。
我们可以通过 reflect.Value.CanSet
来判断一个反射对象是否是可设置的。如果是可设置的,我们就可以通过
reflect.Value.Set
来修改反射对象的值。
这其实也是非常场景的使用反射的一个场景,通过反射来修改变量的值。
1 | var x float64 = 3.4 |
那什么情况下一个反射对象是可设置的呢?前提是这个反射对象是一个指针,然后这个指针指向的是一个可设置的变量。
在我们传递一个值给 reflect.ValueOf
的时候,如果这个值只是一个普通的变量,那么 reflect.ValueOf
会返回一个不可设置的反射对象。
因为这个值实际上被拷贝了一份,我们如果通过反射修改这个值,那么实际上是修改的这个拷贝的值,而不是原来的值。
所以 go 语言在这里做了一个限制,如果我们传递进
reflect.ValueOf
的变量是一个普通的变量,那么在我们设置反射对象的值的时候,会报错。
所以在上面这个例子中,我们传递了 x
的指针变量作为参数。这样,运行时就可以找到 x
本身,而不是
x
的拷贝,所以就可以修改 x
的值了。
但同时我们也注意到了,在上面这个例子中,v.CanSet()
返回的是 false
,而 v.Elem().CanSet()
返回的是
true
。 这是因为,v
是一个指针,而
v.Elem()
是指针指向的值,对于这个指针本身,我们修改它是没有意义的,我们可以设想一下,
如果我们修改了指针变量(也就是修改了指针变量指向的地址),那会发生什么呢?那样我们的指针变量就不是指向
x
了, 而是指向了其他的变量,这样就不符合我们的预期了。所以
v.CanSet()
返回的是 false
。
而 v.Elem().CanSet()
返回的是
true
。这是因为 v.Elem()
才是 x
本身,通过 v.Elem()
修改 x
的值是没有问题的。
Elem 方法
不知道有多少读者和我一样,在初次使用 go 的反射的时候,被
Elem
这个方法搞得一头雾水。 Elem
方法的作用是什么呢?在回答这个问题之前,我们需要明确一点:reflect.Value
和 reflect.Type
这两个反射对象都有 Elem
方法,既然是不同的对象,那么它们的作用自然是不一样的。
reflect.Value 的 Elem 方法
reflect.Value
的 Elem
方法的作用是获取指针指向的值,或者获取接口的动态值。也就是说,能调用
Elem
方法的反射对象,必须是一个指针或者一个接口。
在使用其他类型的 reflect.Value
来调用 Elem
方法的时候,会 panic
:
1 | var a = 1 |
对于指针很好理解,其实作用类似解引用。而对于接口,还是要回到
interface
的结构本身,因为接口里包含了类型和数据本身,所以
Elem
方法就是获取接口的数据部分(也就是 iface
或 eface
中的 data
字段)。
指针类型:
接口类型:
reflect.Type 的 Elem 方法
reflect.Type
的 Elem
方法的作用是获取数组、chan、map、指针、切片关联元素的类型信息,也就是说,对于
reflect.Type
来说, 能调用 Elem
方法的反射对象,必须是数组、chan、map、指针、切片中的一种,其他类型的
reflect.Type
调用 Elem
方法会
panic
。
示例:
1 | t1 := reflect.TypeOf([3]int{1, 2, 3}) // 数组 [3]int |
需要注意的是,如果我们要获取 map 类型 key 的类型信息,需要使用
Key
方法,而不是 Elem
方法。
1 | m := make(map[string]string) |
Interface 方法
这也是非常常用的一个方法,reflect.Value
的
Interface
方法的作用是获取反射对象的动态值。
也就是说,如果反射对象是一个指针,那么 Interface
方法会返回指针指向的值。
简单来说,如果 var i interface{} = x
,那么
reflect.ValueOf(x).Interface()
就是 i
本身,只不过其类型是 interface{}
类型。
Kind
说到反射,不得不提的另外一个话题就是 go 的类型系统,对于开发者来说,我们可以基于基本类型来定义各种新的类型,如:
1 | // Kind 是 int |
但是不管我们定义了多少种类型,在 go 看来都是下面的基本类型中的一个:
1 | type Kind uint |
也就是说,我们定义的类型在 go
的类型系统中都是基本类型的一种,这个基本类型就是 Kind
。
也正因为如此,我们可以通过有限的
reflect.Type
的 Kind
来进行类型判断。
也就是说,我们在通过反射来判断变量的类型的时候,只需要枚举
Kind
中的类型,然后通过 reflect.Type
的
Kind
方法来判断即可。
Type 表示的是反射对象(Type 对象是某一个 Kind,通过 Kind() 方法可以获取 Type 的 Kind),Kind 表示的是 go 底层类型系统中的类型。
比如下面的例子:
1 | func display(path string, v reflect.Value) { |
我们在开发的时候非常常用的结构体,在 go 的类型系统中,通通都是
Struct
这种类型的。
addressable
go 反射中最后一个很重要的话题是 addressable
。在 go
的反射系统中有两个关于寻址的方法:CanAddr
和
CanSet
。
CanAddr
方法的作用是判断反射对象是否可以寻址,也就是说,如果
CanAddr
返回 true
,那么我们就可以通过
Addr
方法来获取反射对象的地址。 如果 CanAddr
返回 false
,那么我们就不能通过 Addr
方法来获取反射对象的地址。对于这种情况,我们就无法通过反射对象来修改变量的值。
但是,CanAddr
是 true
并不是说
reflect.Value
一定就能修改变量的值了。
reflect.Value
还有一个方法 CanSet
,只有
CanSet
返回
true
,我们才能通过反射对象来修改变量的值。
那么 CanAddr
背后的含义是什么呢?它意味着我们传递给
reflect.ValueOf
的变量是不是可以寻址的。
也就是说,我们的反射值对象拿到的是不是变量本身,而不是变量的副本。
如果我们是通过 &v
这种方式来创建反射对象的,那么
CanAddr
就会返回 true
, 反之,如果我们是通过
v
这种方式来创建反射对象的,那么 CanAddr
就会返回 false
。
如果想更详细的了解可以参考一下鸟窝的这篇文章 go addressable 详解。
获取类型信息 - reflect.Type
概述
reflect.Type
是一个接口,它代表了一个类型。我们可以通过
reflect.TypeOf
来获取一个类型的 reflect.Type
对象。 我们使用 reflect.Type
的目的通常是为了获取类型的信息,比如类型是什么、类型的名称、类型的字段、类型的方法等等。
又或者最常见的场景:结构体中的 json
的
tag
,它是没有语义的,它的作用就是为了在序列化的时候,生成我们想要的字段名。
而这个 tag
就是需要通过反射来获取的。
通用的 Type 方法
在 go 的反射系统中,是使用 reflect.Type
这个接口来获取类型信息的。reflect.Type
这个接口有很多方法,下面这些方法是所有的类型通用的方法:
1 | // Type 是 Go 类型的表示。 |
某些类型特定的 Type 方法
下面是某些类型特定的方法,对于这些方法,如果我们使用的类型不对,则会
panic
:
1 | type Type interface { |
创建 reflect.Type 的方式
我们可以通过下面的方式来获取变量的类型信息(创建
reflect.Type
的方式):
获取值信息 - reflect.Value
概述
reflect.Value
是一个结构体,它代表了一个值。 我们使用
reflect.Value
可以实现一些接收多种类型参数的函数,又或者可以让我们在运行时针对值的一些信息来进行修改。
常常用在接收 interface{}
类型参数的方法中,因为参数是接口类型,所以我们可以通过
reflect.ValueOf
来获取到参数的值信息。
比如类型、大小、结构体字段、方法等等。
同时,我们可以对这些获取到的反射值进行修改。这也是反射的一个重要用途。
reflect.Value 的方法
reflect.Value
这个 Sreuct
同样有很多方法:具体可以分为以下几类:
- 设置值的方法:
Set*
:Set
、SetBool
、SetBytes
、SetCap
、SetComplex
、SetFloat
、SetInt
、SetLen
、SetMapIndex
、SetPointer
、SetString
、SetUint
。通过这类方法,我们可以修改反射值的内容,前提是这个反射值得是合适的类型。CanSet 返回 true 才能调用这类方法 - 获取值的方法:
Interface
、InterfaceData
、Bool
、Bytes
、Complex
、Float
、Int
、String
、Uint
。通过这类方法,我们可以获取反射值的内容。前提是这个反射值是合适的类型,比如我们不能通过complex
反射值来调用Int
方法(我们可以通过Kind
来判断类型)。 - map
类型的方法:
MapIndex
、MapKeys
、MapRange
、MapSet
。 - chan
类型的方法:
Close
、Recv
、Send
、TryRecv
、TrySend
。 - slice
类型的方法:
Len
、Cap
、Index
、Slice
、Slice3
。 - struct
类型的方法:
NumField
、NumMethod
、Field
、FieldByIndex
、FieldByName
、FieldByNameFunc
。 - 判断是否可以设置为某一类型:
CanConvert
、CanComplex
、CanFloat
、CanInt
、CanInterface
、CanUint
。 - 方法类型的方法:
Method
、MethodByName
、Call
、CallSlice
。 - 判断值是否有效:
IsValid
。 - 判断值是否是
nil
:IsNil
。 - 判断值是否是零值:
IsZero
。 - 判断值能否容纳下某一类型的值:
Overflow
、OverflowComplex
、OverflowFloat
、OverflowInt
、OverflowUint
。 - 反射值指针相关的方法:
Addr
(CanAddr
为true
才能调用)、UnsafeAddr
、Pointer
、UnsafePointer
。 - 获取类型信息:
Type
、Kind
。 - 获取指向元素的值:
Elem
。 - 类型转换:
Convert
。
Len
也适用于slice
、array
、chan
、map
、string
类型的反射值。
创建 reflect.Value 的方式
我们可以通过下面的方式来获取变量的值信息(创建
reflect.Value
的方式):
总结
reflect
包提供了反射机制,可以在运行时获取变量的类型信息、值信息、方法信息等等。- go 中的
interface{}
实际上包含了两个指针,一个指向类型信息,一个指向值信息。正因如此,我们可以在运行时通过interface{}
来获取变量的类型信息、值信息。 reflect.Type
代表一个类型,reflect.Value
代表一个值。通过reflect.Type
可以获取类型信息,通过reflect.Value
可以获取值信息。- 反射三定律:
- 反射可以将
interface
类型变量转换成反射对象。 - 反射可以将反射对象还原成
interface
对象。 - 如果要修改反射对象,那么反射对象必须是可设置的(
CanSet
)。
- 反射可以将
reflect.Value
和reflect.Type
里面都有Elem
方法,但是它们的作用不一样:reflect.Type
的Elem
方法返回的是元素类型,只适用于 array、chan、map、pointer 和 slice 类型的reflect.Type
。reflect.Value
的Elem
方法返回的是值,只适用于接口或指针类型的reflect.Value
。
- 通过
reflect.Value
的Interface
方法可以获取到反射对象的原始变量,但是是interface{}
类型的。 Type
和Kind
都表示类型,但是Type
是类型的反射对象,Kind
是 go 类型系统中最基本的一些类型,比如int
、string
、struct
等等。- 如果我们想通过
reflect.Value
来修改变量的值,那么reflect.Value
必须是可设置的(CanSet
)。同时如果想要CanSet
为 true,那么我们的变量必须是可寻址的。 - 我们有很多方法可以创建
reflect.Type
和reflect.Value
,我们需要根据具体的场景来选择合适的方法。 reflect.Type
和reflect.Value
里面,都有一部分方法是通用的,也有一部分只适用于特定的类型。如果我们想要调用那些适用于特定类型的方法,那么我们必须先判断reflect.Type
或reflect.Value
的类型(这里说的是Kind
),然后再调用。