深入理解 go reflect - 反射常见错误
go
的反射是很脆弱的,保证反射代码正确运行的前提是,在调用反射对象的方法之前,
先问一下自己正在调用的方法是不是适合于所有用于创建反射对象的原始类型。
go
反射的错误大多数都来自于调用了一个不适合当前类型的方法(比如在一个整型反射对象上调用
Field() 方法)。
而且,这些错误通常是在运行时才会暴露出来,而不是在编译时,如果我们传递的类型在反射代码中没有被覆盖到那么很容易就会
panic。
本文就介绍一下使用 go 反射时很大概率会出现的错误。
获取 Value 的值之前没有判断类型
对于 reflect.Value,我们有很多方法可以获取它的值,比如
Int()、String() 等等。
但是,这些方法都有一个前提,就是反射对象底层必须是我们调用的那个方法对应的类型,否则会
panic,比如下面这个例子:
1 | var f float32 = 1.0 |
上面这个例子中,f 是一个 float32
类型的浮点数,然后我们尝试通过 Int()
方法来获取一个整数,但是这个方法只能用于 int
类型的反射对象,所以会报错。
- 涉及的方法:
Addr,Bool,Bytes,Complex,Int,Uint,Float,Interface;调用这些方法的时候,如果类型不对则会panic。 - 判断反射对象能否转换为某一类型的方法:
CanAddr,CanInterface,CanComplex,CanFloat,CanInt,CanUint。 - 其他类型是否能转换判断方法:
CanConvert,可以判断一个反射对象能否转换为某一类型。
通过 CanConvert
方法来判断一个反射对象能否转换为某一类型:
1 | // true |
如果我们想将反射对象转换为我们的自定义类型,就可以通过
CanConvert 来判断是否能转换,然后再调用
Convert 方法来转换:
1 | type Person struct { |
说明:
reflect.TypeOf(Person{})可以取得Person类型的信息v.Convert可以将v转换为reflect.TypeOf(Person{})指定的类型
没有传递指针给 reflect.ValueOf
如果我们想通过反射对象来修改原变量,就必须传递一个指针,否则会报错(暂不考虑
slice, map,
结构体字段包含指针字段的特殊情况):
1 | func TestReflect(t *testing.T) { |
这个错误的原因是,v 是一个 Person
类型的值,而不是指针,所以我们不能通过
v.FieldByName("Name") 来修改它的字段。
对于反射对象来说,只拿到了 p 的拷贝,而不是 p 本身,所以我们不能通过反射对象来修改 p。
在一个无效的 Value 上操作
我们有很多方法可以创建 reflect.Value,而且这类方法没有
error 返回值,这就意味着,就算我们创建
reflect.Value
的时候传递了一个无效的值,也不会报错,而是会返回一个无效的
reflect.Value:
1 | func TestReflect(t *testing.T) { |
对于这个问题,我们可以通过 IsValid 方法来判断
reflect.Value 是否有效:
1 | func TestReflect(t *testing.T) { |
Field() 方法在传递的索引超出范围的时候,直接 panic,而不会返回一个 invalid 的 reflect.Value。
IsValid 报告反射对象 v 是否代表一个值。
如果 v 是零值,则返回 false。 如果
IsValid 返回 false,则除 String
之外的所有其他方法都将发生 panic。
大多数函数和方法从不返回无效值。
什么时候 IsValid 返回 false
reflect.Value 的 IsValid 的返回值表示
reflect.Value
是否有效,而不是它代表的值是否有效。比如:
1 | var b *int = nil |
在上面这个例子中,v
是有效的,它表示了一个指针,指针指向的对象为 nil。 但是
v.Elem() 和 reflect.Indirect(v)
都是无效的,因为它们表示的是指针指向的对象,而指针指向的对象为
nil。 我们无法基于 nil 来做任何反射操作。
其他情况下 IsValid 返回 false
除了上面的情况,IsValid 还有其他情况下会返回
false:
- 空的反射值对象,获取通过
nil创建的反射对象,其IsValid会返回false。 - 结构体反射对象通过
FieldByName获取了一个不存在的字段,其IsValid会返回false。 - 结构体反射对象通过
MethodByName获取了一个不存在的方法,其IsValid会返回false。 map反射对象通过MapIndex获取了一个不存在的 key,其IsValid会返回false。
示例:
1 | func TestReflect(t *testing.T) { |
注意:还有其他一些情况也会使 IsValid 返回
false,这里只是列出了部分情况。
我们在使用的时候需要注意我们正在使用的反射对象会不会是无效的。
通过反射修改不可修改的值
对于 reflect.Value 对象,我们可以通过
CanSet 方法来判断它是否可以被设置:
1 | func TestReflect(t *testing.T) { |
CanSet 报告 v
的值是否可以更改。只有可寻址(addressable)且不是通过使用未导出的结构字段获得的值才能更改。
如果 CanSet 返回 false,调用 Set
或任何类型特定的 setter(例如
SetBool、SetInt)将
panic。CanSet 的条件是可寻址。
对于传值创建的反射对象,我们无法通过反射对象来修改原变量,CanSet
方法返回 false。
例外的情况是,如果这个值中包含了指针,我们依然可以通过那个指针来修改其指向的对象。
只有通过 Elem 方法的返回值才能设置指针指向的对象。
在错误的 Value 上调用 Elem 方法
reflect.Value 的 Elem() 返回
interface
的反射对象包含的值或指针反射对象指向的值。如果反射对象的
Kind 不是 reflect.Interface 或
reflect.Pointer,它会发生 panic。
如果反射对象为 nil,则返回零值。
我们知道,interface
类型实际上包含了类型和数据。而我们传递给 reflect.ValueOf
的参数就是 interface,所以在反射对象中也提供了方法来获取
interface 类型的类型和数据:
1 | func TestReflect(t *testing.T) { |
在上面的例子中,v 是一个 Person
类型的反射对象,它不是一个指针,所以我们不能通过 v.Elem()
来获取它指向的对象。 而 v1 是一个指针,所以我们可以通过
v1.Elem() 来获取它指向的对象。
调用了一个其类型不能调用的方法
这可能是最常见的一类错误了,因为在 go 的反射系统中,我们调用的一些方法又会返回一个相同类型的反射对象,但是这个新的反射对象可能是一个不同的类型了。同时返回的这个反射对象是否有效也是未知的。
在 go 中,反射有两大对象 reflect.Type 和
reflect.Value,它们都存在一些方法只适用于某些特定的类型,也就是说,
在 go
的反射设计中,只分为了类型和值两大类。但是实际的
go 中的类型就有很多种,比如
int、string、struct、interface、slice、map、chan、func
等等。
我们先不说 reflect.Type,我们从
reflect.Value 的角度看看,将这么多类型的值都抽象为
reflect.Value 之后,
我们如何获取某些类型值特定的信息呢?比如获取结构体的某一个字段的值,或者调用某一个方法。
这个问题很好解决,需要获取结构体字段是吧,那给你提供一个
Field() 方法,需要调用方法吧,那给你提供一个
Call() 方法。
但是这样一来,有另外一个问题就是,如果我们的
reflect.Value 是从一个 int 类型的值创建的,
那么我们调用 Field() 方法就会发生 panic,因为
int 类型的值是没有 Field() 方法的:
1 | func TestReflect(t *testing.T) { |
至于有哪些方法是某些类型特定的,可以参考一下下面两个文档:
总结
- 在调用
Int()、Float()等方法时,需要确保反射对象的类型是正确的类型,否则会panic,比如在一个flaot类型的反射对象上调用Int()方法就会panic。 - 如果想修改原始的变量,创建
reflect.Value时需要传入原始变量的指针。 - 如果
reflect.Value的IsValid()方法返回false,那么它就是一个无效的反射对象,调用它的任何方法都会panic,除了String方法。 - 对于基于值创建的
reflect.Value,如果想要修改它的值,我们无法调用这个反射对象的Set*方法,因为修改一个变量的拷贝没有任何意义。 - 同时,我们也无法通过
reflect.Value去修改结构体中未导出的字段,即使我们创建reflect.Value时传入的是结构体的指针。 Elem()只可以在指针或者interface类型的反射对象上调用,否则会panic,它的作用是获取指针指向的对象的反射对象,又或者获取接口data的反射对象。reflect.Value和reflect.Type都有很多类型特定的方法,比如Field()、Call()等,这些方法只能在某些类型的反射对象上调用,否则会panic。