golang reflect包,反射学习与实践

byte · · 614 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

Go 语言反射的三大法则,其中包括:

  • 从 interface{} 变量可以反射出反射对象;
  • 从反射对象可以获取 interface{} 变量;
  • 要修改反射对象,其值必须可设置;

从反射对象到接口值的过程就是从接口值到反射对象的镜面过程,两个过程都需要经历两次转换:

  • 从接口值到反射对象:

    • 从基本类型到接口类型的类型转换;
    • 从接口类型到反射对象的转换;
  • 从反射对象到接口值:

    • 反射对象转换成接口类型;
    • 通过显式类型转换变成原始类型;

Type,Value

反射包中的所有方法基本都是围绕着TypeValue这两个类型设计的。我们通过reflect.TypeOfreflect.ValueOf可以将一个普通的变量转换成『反射』包中提供的TypeValue,随后就可以使用反射包中的方法对它们进行复杂的操作。

类型 Type 是反射包定义的一个接口,我们可以使用 reflect.TypeOf 函数获取任意变量的的类型,Type 接口中定义了一些有趣的方法,MethodByName 可以获取当前类型对应方法的引用、Implements 可以判断当前类型是否实现了某个接口:

type Type interface {
    // 变量的内存对齐,返回 rtype.align
    Align() int

    // struct 字段的内存对齐,返回 rtype.fieldAlign
    FieldAlign() int

    // 根据传入的 i,返回方法实例,表示类型的第 i 个方法
    Method(int) Method

    // 根据名字返回方法实例,这个比较常用
    MethodByName(string) (Method, bool)

    // 返回类型方法集中可导出的方法的数量
    NumMethod() int

    // 只返回类型名,不含包名
    Name() string

    // 返回导入路径,即 import 路径
    PkgPath() string

    // 返回 rtype.size 即类型大小,单位是字节数
    Size() uintptr

    // 返回类型名字,实际就是 PkgPath() + Name()
    String() string

    // 返回 rtype.kind,描述一种基础类型
    Kind() Kind

    // 检查当前类型有没有实现接口 u
    Implements(u Type) bool

    // 检查当前类型能不能赋值给接口 u
    AssignableTo(u Type) bool

    // 检查当前类型能不能转换成接口 u 类型
    ConvertibleTo(u Type) bool

    // 检查当前类型能不能做比较运算,其实就是看这个类型底层有没有绑定 typeAlg 的 equal 方法。
    // 打住!不要去搜 typeAlg 是什么,不然你会陷进去的!先把本文看完。
    Comparable() bool

    // 返回类型的位大小,但不是所有类型都能调这个方法,不能调的会 panic
    Bits() int

    // 返回 channel 类型的方向,如果不是 channel,会 panic
    ChanDir() ChanDir

    // 返回函数类型的最后一个参数是不是可变数量的,"..." 就这样的,同样,如果不是函数类型,会 panic
    IsVariadic() bool

    // 返回所包含元素的类型,只有 Array, Chan, Map, Ptr, Slice 这些才能调,其他类型会 panic。
    // 这不是废话吗。。其他类型也没有包含元素一说。
    Elem() Type

    // 返回 struct 类型的第 i 个字段,不是 struct 会 panic,i 越界也会 panic
    Field(i int) StructField

    // 跟上边一样,不过是嵌套调用的,比如 [1, 2] 就是说返回当前 struct 的第1个struct 的第2个字段,适用于 struct 本身嵌套的类型
    FieldByIndex(index []int) StructField

    // 按名字找 struct 字段,第二个返回值 ok 表示有没有
    FieldByName(name string) (StructField, bool)

    // 按函数名找 struct 字段,因为 struct 里也可能有类型是 func 的嘛
    FieldByNameFunc(match func(string) bool) (StructField, bool)
    
    // 返回函数第 i 个参数的类型,不是 func 会 panic
    In(i int) Type

    // 返回 map 的 key 的类型,不是 map 会 panic
    Key() Type

    // 返回 array 的长度,不是 array 会 panic
    Len() int

    // 返回 struct 字段数量,不是 struct 会 panic
    NumField() int

    // 返回函数的参数数量,不是 func 会 panic
    NumIn() int

    // 返回函数的返回值数量,不是 func 会 panic
    NumOut() int

    // 返回函数第 i 个返回值的类型,不是 func 会 panic
    Out(i int) Type
}

反射包中 Value 的类型与 Type 不同,它被声明成了结构体。这个结构体没有对外暴露的字段,但是提供了获取或者写入数据的方法:

type Value struct {
    // 反射出来此值的类型,rtype 是啥往上看,但可别弄错了,这 typ 是未导出的,从外部调不到 Type 接口的方法
    typ *rtype

    // 数据形式的指针值
    ptr unsafe.Pointer

    // 保存元数据
    flag
}
// 前提 v 是一个 func,然后调用 v,并传入 in 参数,第一个参数是 in[0],第二个是 in[1],以此类推
func (v Value) Call(in []Value) []Value

// 返回 v 的接口值或者指针
func (v Value) Elem() Value

// 前提 v 是一个 struct,返回第 i 个字段,这个主要用于遍历
func (v Value) Field(i int) Value

// 前提 v 是一个 struct,根据字段名直接定位返回
func (v Value) FieldByName(name string) Value

// 前提 v 是 Array, Slice, String 之一,返回第 i 个元素,主要也是用于遍历,注意不能越界
func (v Value) Index(i int) Value

// 判断 v 是不是 nil,只有 chan, func, interface, map, pointer, slice 可以用,其他类型会 panic
func (v Value) IsNil() bool

// 判断 v 是否合法,如果返回 false,那么除了 String() 以外的其他方法调用都会 panic,事前检查是必要的
func (v Value) IsValid() bool

// 前提 v 是个 map,返回对应 value
func (v Value) MapIndex(key Value)

// 前提 v 是个 map,返回所有 key 组成的一个 slice
func (v Value) MapKeys() []Value

// 前提 v 是个 struct,返回字段个数
func (v Value) NumField() int

// 赋值
func (v Value) Set(x Value)

// 类型
func (v Value) Type() Type

实践

  • 遍历一个结构体的字段以及对应的值

package main

  import (
      "fmt"
      "reflect"
  )

  type Person struct {
      Name     string
      Sex      string
      Age      int
      PhoneNum string
      School   string
      City     string
  }

func main() {
      p1 := Person{
          Name:     "tom",
          Sex:      "male",
          Age:      10,
          PhoneNum: "1000000",
          School:   "spb-kindergarden",
          City:     "cq",
      }

      rv := reflect.ValueOf(p1)
      rt := reflect.TypeOf(p1)
      if rv.Kind() == reflect.Struct {
          for i := 0; i < rt.NumField(); i++ {
              //按顺序遍历
              fmt.Printf("field:%+v,value:%+v\n", rt.Field(i).Name, rv.Field(i))
          }
      }
  }

  • 若知道字段名,直接去取该字段:

    rv := reflect.ValueOf(p1)
    rt := reflect.TypeOf(p1)
    //可以直接取想要的字段
    //reflect的type interface,FieldByName方法会返回字段信息以及是否有该字段;
      if f, ok := rt.FieldByName("Age"); ok {
          fmt.Printf("field:%+v,value:%+v\n", f.Name, rv.FieldByName("Age"))
      }

字段信息是一个结构体,它描述了该字段的下列属性:

// A StructField describes a single field in a struct.
  type StructField struct {
      // Name is the field name.
      Name string
      // PkgPath is the package path that qualifies a lower case (unexported)
      // field name. It is empty for upper case (exported) field names.
      // See https://golang.org/ref/spec#Uniqueness_of_identifiers
      PkgPath string

      Type      Type      // field type
      Tag       StructTag // field tag string
      Offset    uintptr   // offset within struct, in bytes
      Index     []int     // index sequence for Type.FieldByIndex
      Anonymous bool      // is an embedded field
  }

  • 判断一个变量的类型:

      rv := reflect.ValueOf(p1)
      rt := reflect.TypeOf(p1)
      fmt.Printf("kind is %+v\n", rt.Kind())
      fmt.Printf("kind is %+v\n", rv.Kind())

type和value的Kind()方法都可以返回该变量的类型,不过若取得value
后发现其是一个零值,那么会返回Kind为Invalid

  // Kind returns v's Kind.
  // If v is the zero Value (IsValid returns false),        Kind returns Invalid.
  func (v Value) Kind() Kind {
      return v.kind()
  }

reflect的Kind一共有27种类型,基本揽括了所有golang中的类型

const (
      Invalid Kind = iota
      Bool
      Int
      Int8
      Int16
      Int32
      Int64
      Uint
      Uint8
      Uint16
      Uint32
      Uint64
      Uintptr
      Float32
      Float64
      Complex64
      Complex128
      Array
      Chan
      Func
      Interface
      Map
      Ptr
      Slice
      String
      Struct
      UnsafePointer
  )

之前项目中遇到一个问题,如何判断一个变量是否为slice类型,用类型断言不太好做,就可以用reflect的Kind()方法去判断;或者要判断一个变量是slice还是array,也可以通过反射去做;

通过这种方式,我们用

switch rt.Kind() {
case reflect.Int:
    ...
case reflect.String:
    ...
default:
    ...
...
} 

我们就可以实现不同类型走不同分支去处理

  • 获取tag的值

      type TagTest struct {
          Name string `json:"name_json"`
          Age  int    `json:"age_json"`
      }

      t := TagTest{Name: "tom", Age: 10}
      rtt := reflect.TypeOf(t)
      //rtv := reflect.ValueOf(t)
      for i := 0; i < rtt.NumField(); i++ {
          field := rtt.Field(i)
          if json, ok := field.Tag.Lookup("json"); ok {
              fmt.Printf("tag is %+v, value is %+v\n", json, field.Tag.Get("json"))
          }
      }

注意,field.Tag.Lookup()和field.Tag.Get()方法都是取tag的值,只不过Lookup会用第二个返回值返回是否存在这个tag,而Get方法若不存在这个tag会返回一个空字符串


  • 动态调用方法

*T有方法Add

  type T struct{}

  func (t *T) Add(a, b int) {
      fmt.Printf("a + b is %+v\n", a+b)
  }

动态调用

     funcName := "Add"
     typeT := &T{}
     a := reflect.ValueOf(1)
     b := reflect.ValueOf(2)
     in := []reflect.Value{a, b}
     reflect.ValueOf(typeT).MethodByName(funcName).Call(in)

  • 动态调用含返回值的方法

  func (t *T) AddRetErr(a, b int) (int, error) {
      if a+b < 10 {
          return a + b, errors.New("total lt 10")
      }
      return a + b, nil
  }

调用

      funcName = "AddRetErr"
      ret := reflect.ValueOf(typeT).MethodByName(funcName).Call(in)
      fmt.Printf("ret is %+v\n", ret)
      for i := 0; i < len(ret); i++ {
          fmt.Printf("ret index:%+v, type:%+v, value:%+v\n", i, ret[i].Kind(), ret[i].Interface())
      }

这里的ret[i].Kind(),若非基础类型,会得到interface

如果err不是nil,

      if v, ok := ret[1].Interface().(error); ok {
          fmt.Printf("v is %+v\n", v)
      }

类型断言会成功,可以用这种方式去判断返回的error是否为空


  • 通过反射修改值

不是所有的反射值都可以修改。对于一个反射值是否可以修改,可以通过CanSet()进行检查。

要修改值,必须满足:

  • 可以寻址
  • 可寻址的类型:

    • 指针指向的具体元素
    • slice的元素
    • 结构体指针的字段
    • 数组指针的元素

1.指针指向的具体元素

需要三步:

  • 取地址:v := reflect.ValueOf(&x)
  • 判断v.Elem()是否可以设值
  • v.Elem()设置具体值
      ta := 10
      vta := reflect.ValueOf(&ta)
      if vta.Elem().CanSet() {
          vta.Elem().Set(reflect.ValueOf(11))
      }
      fmt.Println("cant set")
      fmt.Printf("vta is :%+v\n", vta.Elem())

2.slice中的元素

      ts := []int{1, 2, 3}
      tsV := reflect.ValueOf(ts)
      if tsV.Index(0).CanSet() {
          tsV.Index(0).Set(reflect.ValueOf(10))
      }
      fmt.Printf("ts is %+v\n", ts)
      //输出:ts is [10 2 3]

3.结构体指针的字段

      t1 := TagTest{}
      tV := reflect.ValueOf(t)
      
      //结构体指针
      t1V := reflect.ValueOf(&t1)
      
      fmt.Printf("tV:%+v\n", tV)
      for i := 0; i < tV.NumField(); i++ {
          val := tV.Field(i)
          t1V.Elem().Field(i).Set(val)
      }
      fmt.Printf("t1 is %+v\n", t1)

4.数组指针的元素

      tsA := [3]int{1, 2, 3}
      tsAv := reflect.ValueOf(&tsA)
      if tsAv.Elem().Index(0).CanSet() {
          tsAv.Elem().Index(0).Set(reflect.ValueOf(10))
      }
      fmt.Printf("tsA is %+v\n", tsA)

参考文章

https://segmentfault.com/a/11...
https://blog.csdn.net/lanyang...
https://draveness.me/golang/d...


有疑问加站长微信联系(非本文作者)

本文来自:Segmentfault

感谢作者:byte

查看原文:golang reflect包,反射学习与实践

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

614 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传