文章目录
go
的反射是很脆弱的,保证反射代码正确运行的前提是,在调用反射对象的方法之前, 先问一下自己正在调用的方法是不是适合于所有用于创建反射对象的原始类型。
go 反射的错误大多数都来自于调用了一个不适合当前类型的方法(比如在一个整型反射对象上调用 Field() 方法)
。而且,这些错误通常是在运行时才会暴露出来,而不是在编译时,如果我们传递的类型在反射代码中没有被覆盖到那么很容易就会 panic
。
本文就介绍一下使用 go
反射时很大概率会出现的错误。
获取 Value 的值之前没有判断类型
对于 reflect.Value
,我们有很多方法可以获取它的值,比如Int()、String()
等等。但是,这些方法都有一个前提,就是反射对象底层必须是我们调用的那个方法对应的类型,否则会 panic
,比如下面这个例子:
var f float32 = 1.0
v := reflect.ValueOf(f)
// 报错:panic: reflect: call of reflect.Value.Int on float32 Value
fmt.Println(v.Int())
上面这个例子中,f
是一个 float32
类型的浮点数,然后我们尝试通过 Int()
方法来获取一个整数,但是这个方法只能用于int
类型的反射对象,所以会报错。
涉及的方法:Addr, Bool, Bytes, Complex, Int, Uint, Float, Interface
;调用这些方法的时候,如果类型不对则会 panic
。
正确使用姿势,使用以上方法前先判断能否转换。
判断反射对象能否转换为某一类型的方法:CanAddr, CanInterface, CanComplex, CanFloat, CanInt, CanUint
。
其他类型是否能转换判断方法:CanConvert
,可以判断一个反射对象能否转换为某一类型。
通过 CanConvert
方法来判断一个反射对象能否转换为某一类型:
// true
fmt.Println(v.CanConvert(reflect.TypeOf(1.0)))
如果我们想将反射对象转换为我们的自定义类型,就可以通过 CanConvert
来判断是否能转换,然后再调用 Convert
方法来转换:
type Person struct {
Name string
}
func TestReflect(t *testing.T) {
p := Person{Name: "foo"}
v := reflect.ValueOf(p)
// v 可以转换为 Person 类型
assert.True(t, v.CanConvert(reflect.TypeOf(Person{})))
// v 可以转换为 Person 类型
p1 := v.Convert(reflect.TypeOf(Person{}))
assert.Equal(t, "foo", p1.Interface().(Person).Name)
}
说明:
reflect.TypeOf(Person{})
可以取得Person
类型的信息v.Convert
可以将v
转换为reflect.TypeOf(Person{})
指定的类型
没有传递指针给 reflect.ValueOf
如果我们想通过反射对象来修改原变量,就必须传递一个指针,否则会报错(暂不考虑 slice
, map
, 结构体字段包含指针字段的特殊情况):
func TestReflect(t *testing.T) {
p := Person{Name: "foo"}
v := reflect.ValueOf(p)
// 报错:panic: reflect: reflect.Value.SetString using unaddressable value
v.FieldByName("Name").SetString("bar")
}
这个错误的原因是,v
是一个 Person
类型的值,而不是指针,所以我们不能通过 v.FieldByName("Name")
来修改它的字段。
对于反射对象来说,只拿到了p
的拷贝,而不是 p
本身,所以我们不能通过反射对象来修改 p
。
在一个无效的 Value 上操作
我们有很多方法可以创建 reflect.Value
,而且这类方法没有 error
返回值,这就意味着,就算我们创建 reflect.Value
的时候传递了一个无效的值,也不会报错,而是会返回一个无效的 reflect.Value
:
func TestReflect(t *testing.T) {
var p = Person{}
v := reflect.ValueOf(p)
// Person 不存在 foo 方法
// FieldByName 返回一个表示 Field 的反射对象 reflect.Value
v1 := v.FieldByName("foo")
assert.False(t, v1.IsValid())
// v1 是无效的,只有 String 方法可以调用
// 其他方法调用都会 panic
assert.Panics(t, func() {
// panic: reflect: call of reflect.Value.NumMethod on zero Value
fmt.Println(v1.NumMethod())
})
}
对于这个问题,我们可以通过 IsValid
方法来判断 reflect.Value
是否有效:
func TestReflect(t *testing.T) {
var p = Person{}
v := reflect.ValueOf(p)
v1 := v.FieldByName("foo")
// 通过 IsValid 判断 reflect.Value 是否有效
if v1.IsValid() {
fmt.Println("p has foo field")
} else {
fmt.Println("p has no foo field")
}
}
Field() 方法在传递的索引超出范围的时候,直接 panic,而不会返回一个 invalid 的 reflect.Value。
IsValid
报告反射对象v
是否代表一个值。如果 v
是零值,则返回 false
。如果 IsValid
返回 false
,则除 String
之外的所有其他方法都将发生 panic
。大多数函数和方法从不返回无效值。
什么时候 IsValid 返回 false
reflect.Value
的 IsValid
的返回值表示 reflect.Value
是否有效,而不是它代表的值是否有效。比如:
var b *int = nil
v := reflect.ValueOf(b)
fmt.Println(v.IsValid()) // true
fmt.Println(v.Elem().IsValid()) // false
fmt.Println(reflect.Indirect(v).IsValid()) // false
在上面这个例子中,v
是有效的,它表示了一个指针,指针指向的对象为 nil
。但是 v.Elem()
和 reflect.Indirect(v)
都是无效的,因为它们表示的是指针指向的对象,而指针指向的对象为 nil
。我们无法基于nil
来做任何反射操作。
除了上面的情况,IsValid
还有其他情况下会返回 false
:
- 空的反射值对象,获取通过
nil
创建的反射对象,其IsValid
会返回false
。 - 结构体反射对象通过
FieldByName
获取了一个不存在的字段,其IsValid
会返回false
。 - 结构体反射对象通过
MethodByName
获取了一个不存在的方法,其IsValid
会返回false
。 map
反射对象通过MapIndex
获取了一个不存在的key
,其IsValid
会返回false
。
示例:
func TestReflect(t *testing.T) {
// 空的反射对象
fmt.Println(reflect.Value{}.IsValid()) // false
// 基于 nil 创建的反射对象
fmt.Println(reflect.ValueOf(nil).IsValid()) // false
s := struct{}{}
// 获取不存在的字段
fmt.Println(reflect.ValueOf(s).FieldByName("").IsValid()) // false
// 获取不存在的方法
fmt.Println(reflect.ValueOf(s).MethodByName("").IsValid()) // false
m := map[int]int{}
// 获取 map 的不存在的 key
fmt.Println(reflect.ValueOf(m).MapIndex(reflect.ValueOf(3)).IsValid())
}
注意:还有其他一些情况也会使 IsValid 返回 false,这里只是列出了部分情况。我们在使用的时候需要注意我们正在使用的反射对象会不会是无效的。
通过反射修改不可修改的值
对于 reflect.Value
对象,我们可以通过 CanSet
方法来判断它是否可以被设置:
func TestReflect(t *testing.T) {
p := Person{Name: "foo"}
// 传递值来创建的反射对象,
// 不能修改其值,因为它是一个副本
v := reflect.ValueOf(p)
assert.False(t, v.CanSet())
assert.False(t, v.Field(0).CanSet())
// 下面这一行代码会 panic:
// panic: reflect: reflect.Value.SetString using unaddressable value
// v.Field(0).SetString("bar")
// 指针反射对象本身不能修改,
// 其指向的对象(也就是 v1.Elem())可以修改
v1 := reflect.ValueOf(&p)
assert.False(t, v1.CanSet())
assert.True(t, v1.Elem().CanSet())
}
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
类型的类型和数据:
func TestReflect(t *testing.T) {
p := Person{Name: "foo"}
v := reflect.ValueOf(p)
// 下面这一行v.Elem()会报错:
// panic: reflect: call of reflect.Value.Elem on struct Value
// v.Elem()
fmt.Println(v.Type())
// v1 是 *Person 类型的反射对象,是一个指针
v1 := reflect.ValueOf(&p)
fmt.Println(v1.Elem(), v1.Type())
}
在上面的例子中,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()
方法的:
func TestReflect(t *testing.T) {
p := Person{Name: "foo"}
v := reflect.ValueOf(p)
// 获取反射对象的 Name 字段
assert.Equal(t, "foo", v.Field(0).String())
var i = 1
v1 := reflect.ValueOf(i)
assert.Panics(t, func() {
// 下面这一行会 panic:
// v1 没有 Field 方法
fmt.Println(v1.Field(0).String())
})
}
总结
- 在调用
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
。