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())
   })
}

至于有哪些办法是某些类型特定的,能够参阅一下下面两个文档:

  • 类型特定的 reflect.Value 办法
  • 类型特定的 reflect.Type 办法

总结

  • 在调用 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