深入理解 go reflect - 反射常见错误

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.ValueIsValid 的返回值表示 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

除了上面的情况,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(例如 SetBoolSetInt)将 panicCanSet 的条件是可寻址。

对于传值创建的反射对象,我们无法通过反射对象来修改原变量,CanSet 方法返回 false
例外的情况是,如果这个值中包含了指针,我们依然可以通过那个指针来修改其指向的对象。

只有通过 Elem 方法的返回值才能设置指针指向的对象。

在错误的 Value 上调用 Elem 方法

reflect.ValueElem() 返回 interface 的反射对象包含的值或指针反射对象指向的值。如果反射对象的 Kind 不是 reflect.Interfacereflect.Pointer,它会发生 panic。 如果反射对象为 nil,则返回零值。

我们知道,interface 类型实际上包含了类型和数据。而我们传递给 reflect.ValueOf 的参数就是 interface,所以在反射对象中也提供了方法来获取 interface 类型的类型和数据:

func TestReflect(t *testing.T) {
	p := Person{Name: "foo"}

	v := reflect.ValueOf(p)

	// 下面这一行会报错:
	// 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.Typereflect.Value,它们都存在一些方法只适用于某些特定的类型,也就是说,
在 go 的反射设计中,只分为了类型两大类。但是实际的 go 中的类型就有很多种,比如 intstringstructinterfaceslicemapchanfunc 等等。

我们先不说 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())
	})
}

至于有哪些方法是某些类型特定的,可以参考一下我的另一篇文章《深入理解 go reflect - 反射基本原理》

总结

  • 在调用 Int()Float() 等方法时,需要确保反射对象的类型是正确的类型,否则会 panic,比如在一个 flaot 类型的反射对象上调用 Int() 方法就会 panic
  • 如果想修改原始的变量,创建 reflect.Value 时需要传入原始变量的指针。
  • 如果 reflect.ValueIsValid() 方法返回 false,那么它就是一个无效的反射对象,调用它的任何方法都会 panic,除了 String 方法。
  • 对于基于值创建的 reflect.Value,如果想要修改它的值,我们无法调用这个反射对象的 Set* 方法,因为修改一个变量的拷贝没有任何意义。
  • 同时,我们也无法通过 reflect.Value 去修改结构体中未导出的字段,即使我们创建 reflect.Value 时传入的是结构体的指针。
  • Elem() 只可以在指针或者 interface 类型的反射对象上调用,否则会 panic,它的作用是获取指针指向的对象的反射对象,又或者获取接口 data 的反射对象。
  • reflect.Valuereflect.Type 都有很多类型特定的方法,比如 Field()Call() 等,这些方法只能在某些类型的反射对象上调用,否则会 panic

你可能感兴趣的:(go,golang,开发语言,后端)