在之前的两篇文章 《深化了解 go reflect – 反射根本原理》、《深化了解 go reflect – 要不要传指针》 中,
咱们讲解了关于 go 反射的一些根本原理,以及经过反射目标修正变量的一些留意事项。
本篇文章将介绍一些常见的反射用法,涵盖了常见的数据类型的反射操作。

依据类型做不同处理

运用反射很常见的一个场景便是依据类型做不同处理,比方下面这个办法,依据不同的 Kind 回来不同的字符串表示:

func getType(i interface{}) string {
   v := reflect.ValueOf(i)
   switch v.Kind() {
   case reflect.Bool:
      b := "false"
      if v.Bool() {
         b = "true"
      }
      return fmt.Sprintf("bool: %s", b)
   case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
      return fmt.Sprintf("int: %d", v.Int())
   case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
      return fmt.Sprintf("uint: %d", v.Uint())
   case reflect.Float32, reflect.Float64:
      return fmt.Sprintf("float: %.1f", v.Float())
   case reflect.String:
      return fmt.Sprintf("string: %s", v.String())
   case reflect.Interface:
      return fmt.Sprintf("interface: %v", v.Interface())
   case reflect.Struct:
      return fmt.Sprintf("struct: %v", v.Interface())
   case reflect.Map:
      return fmt.Sprintf("map: %v", v.Interface())
   case reflect.Slice:
      return fmt.Sprintf("slice: %v", v.Interface())
   case reflect.Array:
      return fmt.Sprintf("array: %v", v.Interface())
   case reflect.Pointer:
      return fmt.Sprintf("pointer: %v", v.Interface())
   case reflect.Chan:
      return fmt.Sprintf("chan: %v", v.Interface())
   default:
      return "unknown"
   }
}
func TestKind(t *testing.T) {
   assert.Equal(t, "int: 1", getType(1))
   assert.Equal(t, "string: 1", getType("1"))
   assert.Equal(t, "bool: true", getType(true))
   assert.Equal(t, "float: 1.0", getType(1.0))
    arr := [3]int{1, 2, 3}
    sli := []int{1, 2, 3}
    assert.Equal(t, "array: [1 2 3]", getType(arr))
    assert.Equal(t, "slice: [1 2 3]", getType(sli))
}

规范库 json 中的示例

在规范库 encoding/json 中,也有相似的场景,比方下面这个办法,依据不同的 Kind 做不同的处理:

func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc {
   // ... 其他代码
   switch t.Kind() {
   case reflect.Bool:
      return boolEncoder
   case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
      return intEncoder
   case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
      return uintEncoder
   // ...省掉其他 case...
   default:
      return unsupportedTypeEncoder
   }
}

在进行 json 编码的时分,因为不知道传入的参数是什么类型,所以需求依据类型做不同的处理,这儿便是运用反射来做的。
经过判别不同的类型,然后回来不同的 encoder

根本类型的反射

这儿说的根本类型是:int*uint*float*complex*bool 这种类型。

经过反射修正根本类型的值,需求留意的是,传入的参数必须是指针类型,否则会 panic

func TestBaseKind(t *testing.T) {
   // 经过反射修正 int 类型变量的值
   a := 1
   v := reflect.ValueOf(&a)
   v.Elem().SetInt(10)
   assert.Equal(t, 10, a)
   // 经过反射修正 uint16 类型变量的值
   b := uint16(10)
   v1 := reflect.ValueOf(&b)
   v1.Elem().SetUint(20)
   assert.Equal(t, uint16(20), b)
   // 经过反射修正 float32 类型变量的值
   f := float32(10.0)
   v2 := reflect.ValueOf(&f)
   v2.Elem().SetFloat(20.0)
   assert.Equal(t, float32(20.0), f)
}

经过反射修正值的时分,需求经过 Elem() 办法的回来值来修正。

数组类型的反射

经过反射修正数组中元素的值,能够运用 Index 办法获得对应下标的元素,然后再运用 Set 办法修正值:

func TestArray(t *testing.T) {
   // 经过反射修正数组元素的值
   arr := [3]int{1, 2, 3}
   v := reflect.ValueOf(&arr)
   // 修正数组中的第一个元素
   v.Elem().Index(0).SetInt(10)
   assert.Equal(t, [3]int{10, 2, 3}, arr)
}

chan 反射

咱们能够经过反射目标来向 chan 中发送数据,也能够从 chan 中接纳数据:

func TestChan(t *testing.T) {
   // 经过反射修正 chan
   ch := make(chan int, 1)
   v := reflect.ValueOf(&ch)
   // 经过反射目标向 chan 发送数据
   v.Elem().Send(reflect.ValueOf(2))
   // 在反射目标外部从 chan 接纳数据
   assert.Equal(t, 2, <-ch)
}

map 反射

经过反射修正 map 中的值,能够运用 SetMapIndex 办法修正 map 中对应的 key

func TestMap(t *testing.T) {
   // 经过反射修正 map 元素的值
   m := map[string]int{"a": 1}
   v := reflect.ValueOf(&m)
   // 修正 a 的 key,修正其值为 2
   v.Elem().SetMapIndex(reflect.ValueOf("a"), reflect.ValueOf(2))
   // 外部的 m 能够看到反射目标的修正
   assert.Equal(t, 2, m["a"])
}

迭代反射 map 目标

咱们能够经过反射目标的 MapRange 办法来迭代 map 目标:

func TestIterateMap(t *testing.T) {
   // 遍历 map
   m := map[string]int{"a": 1, "b": 2}
   v := reflect.ValueOf(m)
   // 创立 map 迭代器
   iter := v.MapRange()
   // 迭代 map 的元素
   for iter.Next() {
      // a 1
      // b 2
      fmt.Println(iter.Key(), iter.Value())
   }
}

slice 反射

经过反射修正 slice 中的值,能够运用 Index 办法获得对应下标的元素,然后再运用 Set* 办法修正值,跟数组相似:

func TestSlice(t *testing.T) {
   // 经过反射修正 slice 元素的值
   sli := []int{1, 2, 3}
   v := reflect.ValueOf(&sli)
   v.Elem().Index(0).SetInt(10)
   assert.Equal(t, []int{10, 2, 3}, sli)
}

string 反射

关于 string 类型,咱们能够经过其反射目标的 String 办法来修正其内容:

func TestString(t *testing.T) {
   // 经过反射修正字符串的值
   s := "hello"
   v := reflect.ValueOf(&s)
   v.Elem().SetString("world")
   assert.Equal(t, "world", s)
}

interface/Pointer 反射

关于 interfacePointer 类型,咱们能够经过其反射目标的 Elem 办法来修正其内容:

func TestPointer(t *testing.T) {
   a := 1
   // 接口类型
   var i interface{} = &a
   v1 := reflect.ValueOf(i)
   v1.Elem().SetInt(10)
   assert.Equal(t, 10, a)
   // 指针类型
   var p = &a
   v2 := reflect.ValueOf(p)
   v2.Elem().SetInt(20)
   assert.Equal(t, 20, a)
}

这两种类型,咱们都需求经过 Elem 办法来先获取其实践保存的值,然后再修正其值。

结构体的反射

关于 go 中的结构体,反射体系中为咱们供给了许多操作结构体的办法,比方获取结构体的字段、办法、标签、经过反射目标调用其办法等。

先假定咱们有如下结构体:

type Person struct {
   Name string
   Age  int
   sex uint8
}
func (p Person) M1() string {
   return "person m1"
}
func (p *Person) M2() string {
   return "person m2"
}

遍历结构体字段

咱们能够经过 NumField 办法来获取结构体的字段数量,然后经过 Field 办法来获取结构体的字段:

func TestStruct1(t *testing.T) {
   var p = Person{Name: "Tom", Age: 18, sex: 1}
   v := reflect.ValueOf(p)
   // string Tom
   // int 18
   // uint8 1
   for i := 0; i < v.NumField(); i++ {
      fmt.Println(v.Field(i).Type(), v.Field(i))
   }
}

依据称号或索引获取结构体字段

咱们能够依据结构体字段的称号或索引来获取结构体的字段:

func TestStruct2(t *testing.T) {
   var p = Person{Name: "Tom", Age: 18, sex: 1}
   v := reflect.ValueOf(p)
   assert.Equal(t, 18, v.Field(1).Interface())
   assert.Equal(t, 18, v.FieldByName("Age").Interface())
   assert.Equal(t, 18, v.FieldByIndex([]int{1}).Interface())
}

修正结构体字段

咱们能够经过 Field 办法来获取结构体的字段,然后再运用 Set* 办法来修正其值:

func TestStruct2(t *testing.T) {
   var p = Person{Name: "Tom", Age: 18, sex: 1}
   v := reflect.ValueOf(&p)
   v.Elem().FieldByName("Name").SetString("Jack")
   assert.Equal(t, "Jack", p.Name)
}

上面因为 Namestring 类型,所以咱们运用 SetString 办法来修正其值,假如是 int 类型,咱们能够运用 SetInt 办法来修正其值,依此类推。

结构体办法调用

经过反射目标来调用结构体的办法时,需求留意的是,假如咱们需求调用指针接纳者的办法,则需求传递地址

func TestStruct3(t *testing.T) {
   var p = Person{Name: "Tom", Age: 18, sex: 1}
   // 值接纳者(receiver)
   v1 := reflect.ValueOf(p)
   assert.Equal(t, 1, v1.NumMethod())
   // 留意:值接纳者没有 M2 办法
   assert.False(t, v1.MethodByName("M2").IsValid())
   // 经过值接纳者调用 M1 办法
   results := v1.MethodByName("M1").Call(nil)
   assert.Len(t, results, 1)
   assert.Equal(t, "person m1", results[0].Interface())
   // 指针接纳者(pointer receiver)
   v2 := reflect.ValueOf(&p)
   assert.Equal(t, 2, v2.NumMethod())
   // 经过指针接纳者调用 M1 和 M2 办法
   results = v2.MethodByName("M1").Call(nil)
   assert.Len(t, results, 1)
   assert.Equal(t, "person m1", results[0].Interface())
   results = v2.MethodByName("M2").Call(nil)
   assert.Len(t, results, 1)
   assert.Equal(t, "person m2", results[0].Interface())
}

阐明:

  • 结构体参数是值的时分,reflect.ValueOf 回来的反射目标只能调用值接纳者的办法,不能调用指针接纳者的办法。
  • 结构体参数是指针的时分,reflect.ValueOf 回来的反射目标能够调用值接纳者和指针接纳者的办法。
  • 调用 MethodByName 办法时,假如办法不存在,则回来的反射目标的 IsValid 办法回来 false
  • 调用 Call 办法时,假如没有参数,传 nil 参数即可。假如办法没有回来值,则回来的成果切片为空。
  • 调用 Call 办法的参数是 reflect.Value 类型的切片,回来值也是 reflect.Value 类型的切片。

是否完成接口

关于这个,其实有一个更简略的办法,那便是利用接口断言:

func TestStrunct4_0(t *testing.T) {
   type TestInterface interface {
      M1() string
   }
   var p = Person{Name: "Tom", Age: 18, sex: 1}
   v := reflect.ValueOf(p)
   // v.Interface() 回来的是 interface{} 类型
   // v.Interface().(TestInterface) 将 interface{} 类型转换为 TestInterface 类型
   v1, ok := v.Interface().(TestInterface)
   assert.True(t, ok)
   assert.Equal(t, "person m1", v1.M1())
}

别的一个办法是,经过反射目标的 Type 办法获取类型目标,然后调用 Implements 办法来判别是否完成了某个接口:

func TestStruct4(t *testing.T) {
   type TestInterface interface {
      M1() string
   }
   var p = Person{Name: "Tom", Age: 18, sex: 1}
   typ := reflect.TypeOf(p)
   typ1 := reflect.TypeOf((*TestInterface)(nil)).Elem()
   assert.True(t, typ.Implements(typ1))
}

结构体的 tag

这在序列化、反序列化、ORM 库中用得非常多,常见的 validator 库也是经过 tag 来完成的。
下面的比如中,经过获取变量的 Type 就能够获取其 tag 了:

type Person1 struct {
   Name string `json:"name"`
}
func TestStruct5(t *testing.T) {
   var p = Person1{Name: "Tom"}
   typ := reflect.TypeOf(p)
   tag := typ.Field(0).Tag
   assert.Equal(t, "name", tag.Get("json"))
}

修正结构体未导字段

咱们知道,结构体的字段假如首字母小写,则是未导出的,不能被外部包访问。但是咱们能够经过反射修正它:

func TestStruct6(t *testing.T) {
   var p = Person{Name: "Tom", Age: 18, sex: 1}
   v := reflect.ValueOf(&p)
   // 下面这样写会报错:
   // panic: reflect: reflect.Value.SetInt using value obtained using unexported field
   // v.Elem().FieldByName("sex").SetInt(0)
   ft := v.Elem().FieldByName("sex")
   sexV := reflect.NewAt(ft.Type(), unsafe.Pointer(ft.UnsafeAddr())).Elem()
   assert.Equal(t, 1, p.sex)           // 修正前是 1
   sexV.Set(reflect.ValueOf(uint8(0))) // 将 sex 字段修正为 0
   assert.Equal(t, 0, p.sex)           // 修正后是 0
}

这儿经过 NewAt 办法针对 sex 这个未导出的字段创立了一个指针,然后咱们就能够经过这个指针来修正 sex 字段了。

办法的反射

这儿说的办法包含函数和结构体的办法。

入参和回来值

reflect 包中供给了 InOut 办法来获取办法的入参和回来值:

func (p Person) Test(a int, b string) int {
   return a
}
func TestMethod(t *testing.T) {
   var p = Person{Name: "Tom", Age: 18, sex: 1}
   v := reflect.ValueOf(p)
   m := v.MethodByName("Test")
   // 参数个数为 2
   assert.Equal(t, 2, m.Type().NumIn())
   // 回来值个数为 1
   assert.Equal(t, 1, m.Type().NumOut())
   // In(0) 是第一个参数,In(1) 是第二个参数
   arg1 := m.Type().In(0)
   assert.Equal(t, "int", arg1.Name())
   arg2 := m.Type().In(1)
   assert.Equal(t, "string", arg2.Name())
   // Out(0) 是第一个回来值
   ret0 := m.Type().Out(0)
   assert.Equal(t, "int", ret0.Name())
}

阐明:

  • InOut 办法回来的是 reflect.Type 类型,能够经过 Name 办法获取类型称号。
  • NumInNumOut 办法回来的是参数和回来值的个数。
  • reflect.Value 类型的 MethodByName 办法能够获取结构体的办法。

经过反射调用办法

reflect.Value 中关于办法类型的反射目标,有一个 Call 办法,能够经过它来调用办法:

func TestMethod2(t *testing.T) {
   var p = Person{Name: "Tom", Age: 18, sex: 1}
   v := reflect.ValueOf(p)
   // 经过反射调用 Test 办法
   m := v.MethodByName("Test")
   arg1 := reflect.ValueOf(1)
   arg2 := reflect.ValueOf("hello")
   args := []reflect.Value{arg1, arg2}
   rets := m.Call(args)
   assert.Len(t, rets, 1)
   assert.Equal(t, 1, rets[0].Interface())
}

阐明:

  • Call 办法的参数是 []reflect.Value 类型,需求将参数转换为 reflect.Value 类型。
  • Call 办法的回来值也是 []reflect.Value 类型。
  • reflect.Value 类型的 MethodByName 办法能够获取结构体的办法的反射目标。
  • 经过办法的反射目标的 Call 办法能够完成调用办法。

总结

  • 经过 reflect.Kind 能够判别反射目标的类型,Kind 涵盖了 go 中所有的根本类型,所以反射的时分判别 Kind 就足够了。
  • 假如要获取反射目标的值,需求传递指针给 reflect.Value
  • 能够往 chan 的反射目标中发送数据,也能够从 chan 的反射目标中接纳数据。
  • SetMapIndex 办法能够修正 map 中的元素。MapRange 办法能够获取 map 的迭代器。
  • 能够经过 Index 办法获取 slice 的元素,也能够经过 SetIndex 办法修正 slice 的元素。
  • 能够经过 SetString 办法修正 string 的值。
  • 关于 interfacePointer 类型的反射目标,能够经过 Elem 办法获取它们的值,一起也只要经过 Elem 获取到的反射目标能调用 Set* 办法来修正其指向的目标。
  • reflect 包中供给了许多操作结构体的功能:如获取结构体的字段、获取结构体的办法、调用结构体的办法等。咱们运用一些类库的时分,会需求经过结构体的 tag 来设置一些元信息,这些信息只要经过反射才能获取。
  • 咱们能够经过 NewAt 来创立一个指向结构体未导出字段的反射目标,这样就能够修正结构体的未导出字段了。
  • 关于函数和办法,go 的反射体系也供给了许多功能,如获取参数和回来值信息、运用 Call 来调用函数和办法等。