深入理解 go 反射
反射
反射是可以让我们在程序运行时(runtime)访问、检测和修改对象本身状态或行为的一种机制。
GO 反射的基础是interface
和类型系统
:
结合 interface 的结构, 可以看出 go 的 interface 是由 type
和 data
两部分组成的, type
承载类型信息, data
承载类型的数据。详细请参考深入理解 go interface
反射对象 reflect.Type 和 reflect.Value
根据 interface 的结构, go 反射的核心是两个对象,分别是 reflect.Type
和 reflect.Value
。 它们分别代表了 go 语言中的类型和值。我们可以通过 reflect.TypeOf
和 reflect.ValueOf
来获取到一个变量的类型和值。
反射定律
在 go 官方文档The Laws of Reflection中提出了三条反射定律:
- 反射可以将 interface 类型变量转换成反射对象
- 反射可以将反射对象还原成 interface 对象
- 如果要修改反射对象,那么反射对象必须是可设置的(CanSet)
反射可以将 interface 类型变量转换成反射对象
这也是上面讲述的reflect.Type
和reflect.Value
两个反射对象的获取方式1
2
3var a = 1
typeOfA := reflect.TypeOf(a)
valueOfA := reflect.ValueOf(a)反射可以将反射对象还原成 interface 对象
我们可以通过reflect.Value.Interface()
来获取到反射对象的 interface 对象,也就是传递给reflect.ValueOf
的那个变量本身。 不过返回值类型是interface{}
,所以我们需要进行类型断言:1
2i := valueOfA.Interface()
fmt.Println(i.(int))如果要修改反射对象,那么反射对象必须是可设置的(CanSet)
我们可以通过reflect.Value.CanSet
来判断一个反射对象是否是可设置的。如果是可设置的,我们就可以通过reflect.Value.Set
来修改反射对象的值。 这其实也是非常场景的使用反射的一个场景,通过反射来修改变量的值。1
2
3
4var b float64 = 22
v := reflect.ValueOf(&b)
fmt.Println("settability of v:", v.CanSet()) // false
fmt.Println("settability of v:", v.Elem().CanSet()) // true反射对象可设置是什么意思呢?前提是这个反射对象是一个指针,然后这个指针指向的是一个可设置的变量。
在我们传递一个值给 reflect.ValueOf 的时候,如果这个值只是一个普通的变量,那么 reflect.ValueOf 会返回一个不可设置的反射对象。
因为这个值实际上被拷贝了一份,我们如果通过反射修改这个值,那么实际上是修改的这个拷贝的值,而不是原来的值。
所以 go 语言在这里做了一个限制,如果我们传递进 reflect.ValueOf 的变量是一个普通的变量,那么在我们设置反射对象的值的时候,会报错。
所以在上面这个例子中,我们传递了 b 的指针变量作为参数。这样,运行时就可以找到 b 本身,而不是 b 的拷贝,所以就可以修改 b 的值了。
但同时我们也注意到了,在上面这个例子中,v.CanSet()
返回的是 false,而v.Elem().CanSet()
返回的是 true。
这是因为,v 是一个指针,而v.Elem()
是指针指向的值,对于这个指针本身,我们修改它是没有意义的,我们可以设想一下, 如果我们修改了指针变量(也就是修改了指针变量指向的地址),那会发生什么呢?那样我们的指针变量就不是指向 b 了, 而是指向了其他的变量,这样就不符合我们的预期了。所以v.CanSet()
返回的是 false。
而v.Elem().CanSet()
返回的是 true。这是因为 v.Elem() 才是 x 本身,通过 v.Elem() 修改 x 的值是没有问题的。
Elem
reflect.Value 和 reflect.Type 这两个反射对象都有 Elem 方法, 他们的区别是什么
reflect.Value 的 Elem 方法
reflect.Value 的 Elem 方法的作用是获取指针指向的值,或者获取接口的动态值。也就是说,能调用 Elem 方法的反射对象,必须是一个指针或者一个接口。
在使用其他类型的 reflect.Value 来调用 Elem 方法的时候,会 panic:
1 |
|
对于指针类似解引用。而对于接口,还是要回到 interface 的结构本身,因为接口里包含了类型和数据本身,所以 Elem 方法就是获取接口的数据部分(也就是 iface 或 eface 中的 data 字段)
reflect.Type 的 Elem 方法
reflect.Type 的 Elem 方法的作用是获取数组、chan、map、指针、切片
关联元素的类型信息,也就是说,对于 reflect.Type 来说,
能调用 Elem 方法的反射对象,必须是数组、chan、map、指针、切片中的一种,其他类型的 reflect.Type 调用 Elem 方法会 panic
1 |
|
需要特别注意的是,如果要获取 map 类型 key 的类型信息,需要使用 Key 方法,而不是 Elem 方法:
1 |
|
反射的方法
interface
reflect.Value 的 Interface 方法的作用是获取反射对象的动态值。 也就是说,如果反射对象是一个指针,那么 Interface 方法会返回指针指向的值。
King
在 go 中, 所有变量的类型都是下面类型中的一个:
1 |
|
可以通过有限的 reflect.Type 的 Kind 来进行类型判断:
1 |
|
常用的 reflect.Type 方法
reflect.Type
的常用方法:
1 |
|
reflect.Value 方法
reflect.Value 同样有很多方法:具体可以分为以下几类:
- 设置值的方法:SetXXX:
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。